天天看点

Linux学习之旅(16)----进程

程序、进程和线程:

程序:一组指令的有序集合,程序本身没有任何运行的含义,它只是一个静态的实体。

进程:是具有一定独立功能的程序关于某个数据集合上的一次运行活动,是系统进行资源分配和调度的一个独立单位。

线程:线程是进程的一个是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位,线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),一个线程可以创建和撤销另一个线程。

进程和程序的区别:

(1)进程是动态的,而程序是静态的。

(2)进程有一定的生命周期,而程序是指令的集合,本身无“运动“的含义。

(3)一个进程只能对应一个程序,一个程序可以对应一个或多个进程。

进程和线程的区别:

(1)调度的基本单位

在传统的OS(操作系统)进程是作为独立调度和分派的基本单位,所以在传统OS进程是能够运行的基本单位。在每次调度是,进程都需要上下文切换,开销大。为了减少系统的开销,所以新型的OS就引入了线程的概念,将线程作为资源调度和分配的基本单位,因为线程拥有比进程更小的资源,在切换时,切换的代价远小于进程。

(2)并发性

在引入线程的操作系统中,不仅仅进程之间可以并发执行,而且在一个进程中的多个线程也可以并发执行,还允许一个进程的所有线程都并发执行和不同进程的线程也可以并发,这使得传统的OS既有更好的并发行,从而能更加有效的提高系统资源的利用率和和系统的吞吐率。

(3)拥有资源

进程可以拥有资源,并作为操作系统中拥有资源的一个基本单位。而线程本身并不拥有系统资源,而是仅有一点必不可少的、能够保证独立运行的资源O(如:TCB(线程控制块)、程序计数器等)。同时还允许多个线程共享该进程的所拥有的资源。

(4)独立性

同一进程的不同线程之间的独立性要比不同进程之间的独立性低。这是因为,每个线程都有一个独立的地址空间和其他资源,除了共享全局变量之外,不允许其他进程访问,但线程会共享进程的所拥有的资源,所以独立性会变差。

(5)系统开销

线程的出现就是为了解决进程引起的系统开销大的原因。所以在系统开销的方面,线程的系统开销远小于进程。

(6)支持多处理机系统

在处理机系统中,如果采用传统的单线程进程,不管有多少处理机,一个进程就只能运行在一个处理机上,而对于多线程而言,一个进程可以分配在多个线程上同时分配到多个处理机上运行,加快了进程的完成。

进程的特点:

(1)动态性:进程的实质是进程实体的执行过程,因此,动态性就是进程的最常见的特性。

(2)并发行:是指多个进程实体同存于内存中,且能够在一段时间内同时运行。

(3)独立性:进程实体是一个能够独立运行、独立获取资源和独立接受的基本单位,但凡没有简历PCB的程序多没有作为一个独立的单位参与运行。

(4)异步性:进程是按异步方式独立运行的,即按各自独立的,不可以预知的速度向前推进。

进程的基本状态及转换

Linux学习之旅(16)----进程

进程原语

就是由若干条指令组成,用于完成一定功能的一个过程。一般进程原语为一个“原子操作”,即不可能在分割的操作,意思就是要么不做,要么完成,在执行期间是不可打断的。

pid_t  fork();
           

用于创建子进程,子进程会将父进程的0-3g用户空间下的所有东西全部”复制“(如:程序、数据等),进而由操作系统在3-4g的内核空间中为子进程创建一个PCB(进程的唯一标识)。

fork()函数为调用一次返回两次。在父进程中返回子进程的PID,在子进程中返回0。子进程在创建出来之后,会接着父进程没有执行完的部分继续执行(父进执行过的,子进程不会再去执行。)。

再这里解释以下创建是的子进程的”复制“。这里的复制并不是说将父进程的0-3g的东西真的全部拷贝,那么太过于麻烦,同时也没有意义,因为子进程和父进程再0-3g是完全一样的,所有操作系统创建子进程时为其0-3g和父进程的0-3g作了一个映射。也就是说在创建进程时,只为子进程创建了一个PCB(进程控制块)。但当子进程修改某个变量时系统就会单独为子进程复制这个变量,这就是所谓的“读时共享,写时复制。”

这里说明一下,一个程序在运行时,系统会为其分配一个进程,而这个进程的父进程就是运行它的shell bash。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
    pid_t pid;
    //fork()调用一次返回两次。在父进程中返回子进程的id号,在子进程返回0.
    pid=fork();//2个进程,子进程会接着父进程向下执行。
    //pid2=fork();//4个进程,两个进程都会执行
    if(pid>0)
    {
        //getpid()获取当前进程id,
        //getppid()获取当前进程父进程的id
        while(1)
        {
            printf("I am father,my pid:%d\n",getpid());
            printf("I am father,my father Pid:%d\n",getppid());
            sleep(1);
        }
    }
    else if(pid==0)
    {
        while(1)
        {
            printf("I am chrid,my pid:%d\n",getpid());
            printf("I am chrid,my father Pid:%d\n",getppid());
            sleep(3);
        }
    }
    else 
    {
        perror("fork");
        exit(1);
    }
    return 0;
}
           
Linux学习之旅(16)----进程

 当执行fork()完成后,子进程就已经创建成功。

pid_t  getpid();       //获取进程ID
pid_t  getppid();      //获取父进程ID
uid_t  getuid();       //获取实际用户ID
uid_t  geteuid();       //获取有效用户ID
gid_t  getgid();        //获取实际用户组ID
gid_t  getegid();       //获取有效组ID
           

exec族

使用fork()创建出来的进程和如果不作处理和父进程时完全相同,这有什么用那?有什么办法可以让子进程去干不父进程不一样的事情那?这时就用到了exec()函数族。当一个程序调用了一种exec()函数时,该进程的用户空间和数据完全被新程序替代,使用exec()函数并不创建新进程,所以调用exec()函数前后进程的ID号不会发生变化。

在exec()函数族中有六个函数:

int execl(const char *path,const char *arg,...);
int execlp(const char *file,const char *arg,...);
int execle(const char *path,const char *arg,...,char *const envp[]);
int execv(const char* path,char *const argv[]);
int execvp(const char *path,char *const argv[]);
int execve(const char *path,char *const argv[],char *const ebvp[]);
           

exec()函数如何调用成功,就会去执行新的程序,如果失败会返回-1.

l:表示list,即在使用时需要将每一个参数都列出来,使用NULL表示结束,例如:execl()、execlp().

p:表示path,如果寻找不到该文件,会在系统的PATH中寻找。

v:表示vector,需要将参数使用数组的形式构造出来(会使函数看着会比较简洁)。

e:表示environment,可以将一份新的环境变量传给它,会在新的环境变量中寻找当前程序。

实际上,只有execve()函数时真正的系统调用,其他5个都是最终调用execve。6个函数的调用过程。

Linux学习之旅(16)----进程
#include <stdio.h>
#include <unistd.h>
#include <error.h>
int main()
{
    int pid=fork();
    if(pid>0)
    {
        //父进程
        printf("我是进程%d\n",getpid());
        //wait()会等待子进程结束
        int id=wait(NULL);
        printf("进程为:%d的子进程以结束\n",id);
    }
    else if(pid==0)
    {
        //子进程
        printf("我是进程:%d,我的父进程是%d\n",getpid(),getppid());
        //这里需要些绝对路径,NULL表示参数结束。
        int exec=execl("/home/kk/code/c++/process/hello",NULL);
        if(exec==-1)
        {
            perror("execl");
        }
    }
    else
    {
        perror("fork"); 
    }
    return 0;
}
           
Linux学习之旅(16)----进程

fork()函数产生子进程的过程:

       系统先给新的进程分配资源,例如存储数据和代码的空间。然后把原来的进程的所有值都复制到新的新进程中,只有少数值与原来的进程的值不同。相当于克隆了一个自己。

僵尸进程:子进程先于父进程被释放,但父进程没有回收子进程的资源(PCB),这是子进程就会称为“僵尸进程”。

孤儿进程:父进程先于子进程被释放,一般子进程是由父进程负责回收的,但父进程这是已经结束。就会导致子进程成为“孤儿进程”。孤儿进程会自动被1号进程(init)“领养”,这是1号进程会成为孤儿进程的父进程,负责回收“孤儿进程”。

僵尸进程:

#include <stdio.h>
#include <errno.h>
#include <unistd.h>
int main()
{
    int pid=fork();
    if(pid>0)
    {
        //父进程会死循环
        while(1)
        {
            printf("我是父进程,我的ID:%d\n",getpid());
            sleep(3);
        }
    }
    else if(pid==0)
    {
        //子进程在打印完会结束。
        printf("我是子进程,我的ID:%d\n",getpid());
    }
    else
    {
        perror("fork");
    }
    return 0;
}
           
Linux学习之旅(16)----进程

这时的子进程就成为“僵尸进程”。“僵尸进程”是非常危险的,会造成内存泄漏。

wait()和waitpid()

那如何防止僵尸进程的产生那?linux系统提供了wait()和waitpid()函数,这两者都是让父进程回收子进程,不同的是:wait()是阻塞的,即如果子进程没有全部结束,父进程就会一直等待,不会向下继续执行。而waitpid()通过参数的设置,可以将函数转换位非阻塞的。

孤儿进程

#include <stdio.h>
#include <unistd.h>
#include <errno.h>
int main()
{
    int pid=fork();
    if(pid>0)
    {
        printf("我是父进程,我的进程ID为:%d\n",getpid());
        sleep(1);
    }
    else if(pid==0)
    {
        while(1)
        {
            printf("我是子进程,我的进程ID为:%d,我的父进程的ID为:%d\n",getpid(),getppid());
            sleep(3);
        }
    }
    return 0;
}
           
Linux学习之旅(16)----进程

这是的子进程就是一个孤儿进程。当父进程结束时,会导致shell自动从后台移动到前台,这是虽然程序还在运行,但sehll也是也可运行的,应为shell和1号进程不是同一个进程,不会相互影响。