进程的创建

在了解什么是进程中,已经初始 fork 函数了,这里我们对 fork 函数进行一些补充。

fork 的常规用法:

1. 一个父进程希望子进程复制自己,使父子进程同时执行不同的代码段

2. 一个进程要执行⼀个不同的程序

fork 函数是可能调用失败的,fork 调用失败的原因:

1. 系统中有太多的进程

2. 实际用户的进程数超过了限制


在了解进程的概念时,曾提到过“写时拷贝”,接下来将着重讲解写时拷贝是如何实现的。

在页表中代码段是只读的,但是一般调用 fork 函数时,如果父进程要创建子进程,在创建子进程之前,会把自己的数据段由之前的读写权限改成只读,然后子进程一旦继承父进程的页表,数据段就是只读的;如果父进程从来不创建子进程,那么父进程所对应的页表中的数据段权限就是读写,从此父子进程的代码和数据都是只读的。

如果父子进程中有任一进程尝试修改数据段,由于权限是只读的,OS 会识别到用户要对数据段进行修改,所以将数据段的权限在 fork 之前改成只读的,是为了让 OS 知道用户需要对数据段进行修改。一旦修改之后,OS 会检查操作的合法性,即是否产生野指针问题,是否在内存里等等,检查数据段的权限,发现是只读的,但是现在需要修改数据段,那么会在物理内存中开辟新地址,将新的空间的地址填写到进程的页表中,然后再将数据段的权限变成读写,这样就可以修改数据段了。

写时拷贝中最重要的关键环节是 OS 将数据段的权限改成只读,一旦任意一方尝试写入,OS便以写时拷贝的方式各自生成一份副本。


进程的终止

进程终止的本质是释放系统资源,就是释放进程申请的相关内核数据结构和对应的数据和代码。先释放进程的代码和数据,再释放内核数据结构,因为会有僵尸进程。

进程终止会有几种情况?三种!

1. 进程正常结束,结果正确

2. 进程正常结束,结果错误

3. 进程异常终止

先讲解 1,2 情况,1,2情况都是进程正常结束,区别是结果是否正确。那么用户是如何知道进程的结果是否正确?正是如此 main 函数才会返回return 0,return 0不叫做函数的返回值,而叫做进程的退出码!退出码为0表示进程结果正确。

测试代码:

int main()
{
    return 100;
}

如何知道运行结果是否正确?main 函数的返回值会交给当前进程的父进程,当前进程的父进程是谁?bash。bash 会记录上一个进程的退出码,如果要查看上一个进程的退出码,使用指令:echo $?

所以一个进程执行完毕时,结果是否正确,可以通过返回值为 0 来判断,为0结果正确,非0结果错误。如果一个进程执行结果错误,不仅需要告知父进程执行结果错误,还需要告知为什么结果错误!结果正确的原因只有一种,结果错误的原因有很多种,因此会设置不同的退出码,来表示结果错误的不同原因。

但是用户怎么知道这些退出码分别代表的意思?我们可以自定义退出码;其次使用系统提供的。大部分情况下都是使用自定义的退出码,系统指令使用的都是系统的退出码。


errno 函数,表示调用函数时的错误码,strerror(错误码)显示错误码表示的意思。如果想知道系统提供的错误码是哪一些,可以将其打印出来。

示例代码:

for(int i = 0; i < 140; i++)
{
    printf("%d->%s\n", i, strerror(i));
}

部分结果:

如果十分关心进程的退出码,需要好好的表示进程的退出码。

使用 error 和 strerror 函数,使用 error 时,需要引用头文件<error.h>,使用strerror 函数时,需要引用头文件<string.h>,代码:

FILE* pf = fopen("test.txt", "r");
if(pf == NULL)
{
    printf("%d: %s\n", errno, strerror(errno));
}

运行结果:


接下来讲解情况3:进程异常终止。

进程异常终止说明了 return 0 语句是未被执行的,因此退出码是没有意义的为什么进程会异常终止?因为进程被信号终止了

信号列表,使用 kill -l 指令:

其中我们常用的就是1~31号信号,这些是普通信号,进程之所以会被终止,本质是因为进程收到了信号

因此可以逐一说明,进程终止时,三种情况出现的结果:

情况1:进程正常运行,结果正确,即进程运行期间,没有接收到信号,并且退出码为0

情况2:进程正常运行,结果错误,即进程运行期间,没有接收到信号,并且退出码为非0

情况3:进程异常终止,即进程运行期间,接收到了信号,并且退出码无意义

总结:进程执行的结果状态,可以使用两个数字表示:信号(sig)和退出码(exit_code)

这两个数字需要用户记住吗?不需要。当一个进程退出的时,OS 会把进程退出的详细信息写入到进程的 task_struct 中,所以进程退出时,需要进入僵尸状态来维持进程的退出状态。

为什么说进程的退出的详细信息会写入到进程的task_struct中?看源码:

exit_code 保存进程的退出码信息,exit_signal 保存进程的信号信息


进程的退出

如何让进程退出(不考虑进程异常退出的情况)?


第一种方法:main 函数 return。前面已经演示过了。


第二种方法:使用函数 exit。

exit 函数的信息:

功能:使一个正常进程退出。status 是退出码,exit(status)等价于 return 退出码。

演示exit函数的使用,示例代码:

printf("I am a process, pid = %d, ppid = %d\n", getpid(), getppid());
exit(100);

运行结果:

此外还有另一种使用方法。

示例代码:

void Func()
{
  printf("hello linux\n");
  exit(10);
}

int main()
{
    printf("I am a process, pid = %d, ppid = %d\n", getpid(), getppid());
    Func();
    exit(100);

    return 0;
}

在main函数中exit,代表进程结束;在非main函数中exit,代表着什么?

运行结果:

总结:在任意地方使用 exit 函数,非 main 函数 return,表示的是函数结束,若非 main 函数调用exit 函数,表示进程结束


第三种方法:使用 _exit 函数。

_exit 函数的信息:

功能:谁调用 _exit 函数,就终止哪个进程。

使用_exit函数,示例代码:

void Func()
{
  printf("hello linux\n");
  _exit(10);
}

int main()
{
    printf("I am a process, pid = %d, ppid = %d\n", getpid(), getppid());
    Func();
    _exit(100);

    return 0;
}

运行结果:

在终止进程这个功能上,它们看起来确实没有区别,但是在其他方面存在着差异。

示例代码:

printf("hello linux");
sleep(2);

这在前面说过,会先执行 printf 函数,再执行 sleep 函数。但是在打印时,会先睡眠2s,再打印,这是因为 printf 函数的打印内容写入缓冲区中了,进程结束之后,刷新缓冲区,再将 printf 函数的内容打印到显示器上。

在上面的代码中加上 eixt(100),观察其结果:

在上面的代码中加上 _exit(100),观察其结果:

exit 和 _exit 之间的不同:exit 终止进程,会强制刷新内存缓冲区;_exit终止进程,不会。exit 是库函数,_exit 是系统调用,库函数是系统调用的上层,即exit终止进程的功能实现依靠 _exit 函数。然而尽管这样 exit 和 _exit 存在着区别,这说明了缓冲区和刷新缓冲区的操作,一定不存在内核中。推荐使用 exit 函数来退出进程


进程的等待

为什么要进行进程等待?如果不等,会造成内存泄漏问题;父进程可能要获取子进程的退出信息。进程等待可以解决僵尸进程问题。

之前讲过,子进程退出,父进程如果不管不顾,就可能造成“僵尸进程”的问题,进而造成内存泄漏。此外,进程一旦变成僵尸状态,即便是信号也无法杀掉僵尸进程,因为谁也没有办法杀死⼀个已经死去的进程。最后,父进程派给子进程的任务完成的如何,用户是需要知道的,子进程运行完成,结果是否正确或者是否正常退出。父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息。

进程等待的方法:wait 函数waitpid 函数


wait 函数的关键信息

需要引用的头文件:#include <sys/types.h>    #include <sys/wait.h>   

函数原型:pid_t wait(int* status);

返回值:成功返回被等待进程 pid ,失败返回-1 。

参数:输出型参数,获取⼦进程退出状态 , 不关⼼则可以设置成为 NULL

使用 wait 函数,验证进程等待可以解决子进程的僵尸问题。

测试代码:

pid_t id = fork();
if(id == 0)
{
    while(1)
    {
        printf("I am 子进程: pid = %d, ppid = %d\n", getpid(), getppid());
        sleep(2);
    }

    // 直接让子进程终止,进入僵尸状态
    exit(1);
}
else if(id > 0)
{
    pid_t rid = wait(NULL);
    if(rid == id)
    {
        printf("pid = %d 等待成功!\n", getpid()); 
    }
}
else
{}

测试结果:

如果父进程等待子进程,但是子进程没有退出,则父进程会阻塞在 wait 函数中

为了更好的观察到子进程的僵尸状态从无到有,从有到无,示例代码:

pid_t id = fork();
  if(id == 0) 
  {
    int count = 5;
    while(count--)
    {
      printf("I am 子进程: pid = %d, ppid = %d\n", getpid(), getppid());
      sleep(2);
    }

    // 直接让子进程终止,进入僵尸状态
    exit(0);
  }
  else if(id > 0)
  {
    // 子进程需要经过10s后才退出
    // 为了清晰的看到wait确实解决子进程僵尸问题,可以让父进程休眠15s
    sleep(15);
    // 回收子进程,等待子进程的僵尸状态
    int status = 0;
    pid_t rid = wait(NULL);
    // 若rid等于子进程的id,说明父进程等待成功了
    if(rid == id)
    {
      printf("pid = %d 等待成功!\n", getpid());
    }
    // wait成功之后,让父进程休眠5s,在退出
    sleep(5);
    eixt(0);
  } 
  else 
  {}

测试结果:


waitpid 函数的关键信息:

函数原型:pid_ t waitpid(pid_t pid, int *status, int options);

返回值:

当正常返回的时候 waitpid 返回收集到的子进程的进程 ID;如果设置了选项 WNOHANG,而调用中 waitpid 发现没有已退出的子进程可收集,则返回 0;如果调用中出错,则返回-1,这时 errno 会被设置成相应的值以指示错误所在

参数:

pid:pid=-1, 等待任一个子进程,与 wait 等效;pid>0. 等待其进程 ID 与 pid 相等的子进程。

status:输出型参数

WIFEXITED(status):若为正常终⽌⼦进程返回的状态,则为真。(查看进程是否是正常退出)

WEXITSTATUS(status):若 WIFEXITED 非零,提取子进程退出码。(查看进程的退出码)

options:默认为 0 ,表示阻塞等待

WNOHANG:若 pid 指定的⼦进程没有结束,则 waitpid() 函数返回 0,不予以等待。若正常结束,则返回该子进程的 ID

使用 waitpid 函数,验证进程等待可以获取子进程的退出信息。

示例代码:

pid_t id = fork();
  if(id == 0) 
  {
    int count = 5;
    while(count--)
    {
      printf("I am 子进程: pid = %d, ppid = %d\n", getpid(), getppid());
      sleep(2);
    }

    // 直接让子进程终止,进入僵尸状态
    exit(1);
  }
  else if(id > 0)
  {
    // 回收子进程,等待子进程的僵尸状态
    int status = 0;
    pid_t rid = waitpid(id, &status, 0);
    // 若rid等于子进程的id,说明父进程等待成功了
    if(rid == id)
    {
      printf("pid = %d 等待成功! status: %d\n", getpid(), status);
    }
  } 
  else 
  {}

运行结果:

为什么 status 是256?在写子进程的退出码时故意写成 exit(1),为什么最终父进程获取到的子进程的退出信息是256,与退出码1对不上呀?参数 status 是用来获取子进程的退出信息的,而子进程的退出有三种情况,用两个数字表明进程的退出信息,因此 status 获取的不只是子进程的退出码,还会获取子进程的接收的信号。status 变量有32位比特位,将其分成两份,不使用高16位,只使用低16位比特位。

上述测试代码是正常终止,且退出码为1,未接收到信号,信号编号为0,故status的二进制形式为:1,0000,0000,即2^8 = 256,所以 status 获取的子进程的信息是256。

如果想要成功的将进程的退出信息描述出来,即打印退出码和接收到的信号,代码需要这样写:

pid_t id = fork();
if(id == 0)
{
    int count = 5;
    while(count--)
    {
        printf("I am 子进程: pid = %d, ppid = %d\n", getpid(), getppid());
        sleep(2);
    }

    // 直接让子进程终止,进入僵尸状态
    exit(1);
}
else if(id > 0)
{
    int status = 0;
    pid_t rid = waitpid(id, &status, 0);
    // 若rid等于子进程的id,说明父进程等待成功了
    if(rid == id)
    {
        // status >> 8 — 将 status 右移8位,让退出码移动到低8位
        // &0xFF ——取低8位,得到实际的退出码
        int exit_code = (status >> 8)&0xFF;
        // 取低7位,获取的是导致进程终止的信号编号 
        int exit_signal = status&0x7F;
        printf("pid = %d 等待成功! status: %d, eixt_code: %d, exit_signal: %d\n", getpid(), status, exit_code, exit_signal);
    }
}
else 
{}

运行结果:

将子进程设置成死循环,使用信号来终止,示例代码:

pid_t id = fork();
if(id == 0)
{
    while(1)
    {
        printf("I am 子进程: pid = %d, ppid = %d\n", getpid(), getppid());
        sleep(2);
    }

    // 直接让子进程终止,进入僵尸状态
    exit(1);
}
else if(id > 0)
{
    int status = 0;
    pid_t rid = waitpid(id, &status, 0);
    // 若rid等于子进程的id,说明父进程等待成功了
    if(rid == id)
    {
        // status >> 8 — 将 status 右移8位,让退出码移动到低8位
        // &0xFF ——取低8位,得到实际的退出码
        int exit_code = (status >> 8)&0xFF;
        // 取低7位,获取的是导致进程终止的信号编号 
        int exit_signal = status&0x7F;
        printf("pid = %d 等待成功! status: %d, eixt_code: %d, exit_signal: %d\n", getpid(), status, exit_code, exit_signal);
    }
}
else 
{}

测试结果:

这模拟的就是异常退出的情况,退出码无意义,收到的信号为 9。


父进程通过 waitpid 函数,是如何得到子进程的退出信息?

1. 子进程在退出时会进入Z状态,让父进程获取子进程的退出信息

2. 子进程的退出信息会写在自己 PCB 的 exit_code 和 exit_signal 变量中

3. waitpid 通过 status 获取子进程的退出信息,根据 waitpid 参数 pid 找到子进程的是谁,进而父进程可以读取到子进程 PCB 中的退出信息

waitpid 函数中参数 pid 是确定的,可以直接获取子进程的退出信息。如果 pid 写为-1,等待任何一个子进程,那么父进程如何获取子进程的退出信息?进程的 task_struct 中的属性会给出答案!

在进程的 PCB 中,存在子进程列表和父进程列表,遍历子进程列表的的 PCB,查看 PCB 中的state,如果某个进程的 state 为 Z 状态,那么它就是父进程在等待的子进程,获取的就是该子进程的退出信息。


waitpid 创建了两个宏:WIFEXITED 和 WEXITSTATUS,代表的意思为:

#define WIFEXITED(status) (status&0x7F)  // 进程接收到的信号
#define WEXITSTATUS(status) (status >> 8)&0xFF // 进程的退出码

使用演示:

printf("I am a process,pid = %d, ppid = %d\n", getpid(), getppid());

pid_t id = fork();
if(id < 0)
{
    // 打印fork失败的原因
    perror("fork");
    exit(1);
}
else if(id == 0)
{
    // 子进程
    int cnt = 5;
    while(cnt--)
    {
        printf("I am a child process,pid = %d, ppid = %d\n", getpid(), getppid());
        sleep(1);
    }

    exit(0);
}
else
{
    // 父进程
    // 等待任意子进程 --- -1
    // 退出码为空,第三参数默认为0
    int status = 0;
    pid_t wid = waitpid(-1, NULL, 0);
    if(wid > 0)
    {
        if(WIFEXITED(status))
        {
            printf("wait sucess,退出的子进程是: %d, exit_code: %d\n", wid, WEXITSTATUS(status));
        }
        else 
        {
            printf("子进程是异常退出的\n");
        }
    }
    else
    {
        // 等待失败,waitpid返回-1
        printf("ret: %d\n", wid);
        perror("waitpid");
    }
}

运行结果:


fork 创建一个子进程之后,父子进程谁先运行?作为用户的我们,我们是不确定父子进程哪一个先运行,父子进程谁先运行,取决于进程的优先级,谁先放在放在活跃队列里,是调度器说了算。父子进程谁先退出?子进程先退出,父进程负责回收子进程的资源。当子进程没有退出,父进程会阻塞在waitpid函数中,这种等待叫做阻塞等待

waitpid 函数存在一个参数option,option表示等待方式,前面使用 waitpid 函数的时候,都默认option 为0,表示父进程阻塞等待。父进程在阻塞等待期间是什么都不做的,会有点浪费资源。若不想父进程阻塞等待,想让它在等待的期间也可以执行其它的任务,使用 waitpid 函数时,第三个参数使用宏 WONHANG,它会让父进程处于非阻塞等待。WNOHANG 表示非阻塞等待,W——wait,NO —— 非/不要,HANG —— 夯住了(不响应,卡住了)。

如何理解阻塞等待和非阻塞等待?以钓鱼为例子,非阻塞等待:在等待鱼上钩的期间,可以做其它事,看书,刷手机等等,时不时的看看鱼上钩了没;阻塞等待:在等待鱼上钩的期间,就干等着鱼上钩。阻塞等待:父进程只等待一次,直到子进程退出,阻塞等待期间只等待,不做其他事;非阻塞等待:父进程等待多次,在检查到子进程仍未退出,立刻返回,执行其它的工作,之后再次检查子进程是否退出,这就是非阻塞轮询等待

演示非阻塞等待,示例代码:

printf("I am a process,pid = %d, ppid = %d\n", getpid(), getppid());

pid_t id = fork();
if(id < 0)
{
    // 打印fork失败的原因
    perror("fork");
    exit(1);
}
else if(id == 0)
{
    // 子进程
    int cnt = 5;
    while(cnt--)
    {
        printf("I am a child process,pid = %d, ppid = %d\n", getpid(), getppid());
        sleep(1);
    }
    exit(0);
}
else
{
    // 父进程
    while(1)
    {
        // 等待任意子进程 --- -1
        // 退出码为空,第三参数默认为0
        int status = 0;
        pid_t wid = waitpid(id, NULL, WNOHANG); // 非阻塞检测进程并回收
        // 子进程终止了
        if(wid > 0)
        {
            if(WIFEXITED(status))
            {
                printf("wait sucess,退出的子进程是: %d, exit_code: %d\n", wid, WEXITSTATUS(status));
            }
            else 
            {
                printf("子进程是异常退出的\n");
            }
            break;
        }
        // 子进程未终止
        else if(wid == 0)
        {
            printf("子进程仍未退出,父进程还需等待\n");
            sleep(2);
        }
        else 
        {
            printf("waitpid fails, ret: %d", wid);
            perror("waitpid");
            break;
        }
    }
}

运行结果:

非阻塞是函数提供的能力,轮询是用户使用 while 循环提供的功能,非阻塞等待和阻塞等待哪种等待方式更高效?差不多!父进程需要等待多长的时间,不取决于父进程,取决于子进程,子进程什么时候结束,父进程就等待多长的时间。比较哪个等待方式更高效,应该比较的是谁先等待成功,而不是比较谁在等待期间执行的任务的多少。只不过非阻塞等待在等待期间可以执行其它任务,将等待时间利用了起来。就好比钓鱼,比较谁钓鱼的效率高,应该比较的是谁钓的鱼多,而不是比较谁在钓鱼期间,谁干的事多。


如何理解父进程在非阻塞期间可以执行其它任务?它是如何利用等待时间的?使用代码解释。

示例代码:

// 函数指针
typedef void (*call_back)();

// 一些函数
void Func1()
{
    std::cout << "Func1()" << std::endl;
}

void Func2()
{
    std::cout << "Func2()" << std::endl;
}
 
void Func3()
{
    std::cout << "Func3()" << std::endl;
}


int main()
{

    printf("I am a process,pid = %d, ppid = %d\n", getpid(), getppid());

    std::vector<call_back> tasks;
    tasks.push_back(Func1);
    tasks.push_back(Func2);
    tasks.push_back(Func3);

    pid_t id = fork();
    if(id < 0)
    {
        // 打印fork失败的原因
        perror("fork");
        exit(1);
    }
    else if(id == 0)
    {
        // 子进程
        int cnt = 5;
        while(cnt--)
        {
            printf("I am a child process,pid = %d, ppid = %d\n", getpid(), getppid());
            sleep(1);
        }
        exit(0);
    }
    else
    {
        // 父进程
        while(1)
        {
            // 等待任意子进程 --- -1
            // 退出码为空,第三参数默认为0
            int status = 0;
            pid_t wid = waitpid(id, NULL, WNOHANG); // 非阻塞检测进程并回收
            // 子进程终止了
            if(wid > 0)
            {
                if(WIFEXITED(status))
                {
                    printf("wait sucess,退出的子进程是: %d, exit_code: %d\n", wid, WEXITSTATUS(status));
                }
                else 
                {
                    printf("子进程是异常退出的\n");
                }
                break;
            }
            // 子进程未终止
            else if(wid == 0)
            {
                printf("子进程仍未退出,父进程还需等待\n");
                sleep(2);
                // 等待期间执行其它任务
                for(auto& task : tasks)
                {
                    task();
                }
            }
            else 
            {
                printf("waitpid fails, ret: %d", wid);
                perror("waitpid");
                break;
            }
        }
    }

    return 0;
}

运行结果:

不推荐使用非阻塞等待,即不推荐使用 WNOHANG进程等待时推荐使用 waitpid 函数


启停多进程

示例代码:

enum 
{
  right,
  UASGE_ERROR
};

// 子进程要执行的任务
void Task()
{
  int count = 5;
  while(count--)
  {
    printf("I am a child process, pid = %d, ppid = %d, count: %d\n", getpid(), getppid(), count);
    sleep(2);
  }
}

int main(int argc, char* argv[])
{
  if(argc != 2)
  {
    // 规定指令输入的格式
    std::cout << "Uasge: " << argv[0] << " process_num" << std::endl;
    exit(UASGE_ERROR);
  }

  // 获取要创建子进程的数量,即argv[1]
  // 但是获取到的是字符串,怎么转成整型?使用stoi函数
  int num = std::stoi(argv[1]);

  // 创建一批子进程
  for(int i = 0; i < num; i++)
  {
    pid_t id = fork();
    if(id == 0)
    {
      // 子进程
      Task();  // 子进程要执行的任务
      exit(0);
    }
    
    // 等待多进程
    // 父进程
    int status = 0;
    // 等待子进程
    pid_t wid = waitpid(pid, &status, 0);
    if(wid > 0)
    {
      printf("子进程: %d 退出,exit_code: %d\n", wid, WEXITSTATUS(status));
    }
  }

  return 0;
}

这样写存在问题,一开始只有父进程在运行,fork 创建子进程之后,子进程进入 if 判断,执行完毕就退出了,所以到头来就只有父进程在执行,for 循环只有父进程在运行,根本就没有创建一批子进程。我们的目的是先创建一批子进程,然后再让父进程一个一个的回收子进程,按上述所表达的,每创建一个进程,就被父进程回收了。因此创建子进程和等待子进程两个逻辑块需要分开

优化后的代码:

enum 
{
  right,
  UASGE_ERROR
};

// 子进程要执行的任务
void Task()
{
  int count = 5;
  while(count--)
  {
    printf("I am a child process, pid = %d, ppid = %d, count: %d\n", getpid(), getppid(), count);
    sleep(2);
  }
}

int main(int argc, char* argv[])
{
  if(argc != 2)
  {
    // 规定指令输入的格式
    std::cout << "Uasge: " << argv[0] << " process_num" << std::endl;
    exit(UASGE_ERROR);
  }

  // 获取要创建子进程的数量,即argv[1]
  // 但是获取到的是字符串,怎么转成整型?使用stoi函数
  int num = std::stoi(argv[1]);

  // 存储子进程的pid值
  std::vector<pid_t> vid;
  // 创建一批子进程
  for(int i = 0; i < num; i++)
  {
    pid_t id = fork();
    if(id == 0)
    {
      // 子进程
      Task();  // 子进程要执行的任务
      exit(0);
    }
    
    vid.push_back(id);
  }

  // 等待多进程
  // 如何知道要等多少个?并且waitpid函数参数中的子进程id是什么?
  // 使用容器vector,让vector存储每次创建子进程的pid值
  for(auto& pid : vid)
  {
    // 父进程
    int status = 0;
    // 等待子进程
    pid_t wid = waitpid(pid, &status, 0);
    if(wid > 0)
    {
      printf("子进程: %d 退出,exit_code: %d\n", wid, WEXITSTATUS(status));
    }
  }

  return 0;
}

可以将创建多进程和等待多进程逻辑封装成函数。

改进后的代码:

// 习惯规定:
// 输入: 参数使用 const &
// 输出: 参数使用 *
// 输入输出: 参数使用 &
void CreateChileProcess(int num, std::vector<pid_t>* vid)
{
    // 创建一批子进程
    for(int i = 0; i < num; i++)
    {
        pid_t id = fork();
   
        if(id == 0)
        {
            // 子进程
            Task();   // 子进程要执行的任务
            exit(0);
        }

        // vector容器存储子进程的pid值
        vid.push_back(id);
    }
}

void WaitChildProcess(const std::vector<pid_t>& vid)
{  
    // 等待子进程
    for(auto& pid : vid)
    { 
        // 父进程如何知道自己要等多少个子进程?
        // 使用 vector 容器,将每次创建的子进程的pid值存储起来
        // 父进程
        int status = 0;
        pid_t wid = waitpid(-1, &status, 0);
        if(wid > 0)
        {
            printf("子进程: %d 退出,wait_code: %d\n", wid, WEXITSTATUS(status));
            sleep(2);
        }
    }
}

int main(int argc, char* argv[])
{ 
    if(argc != 2)
    {
        // 指令的输入格式,要创建5个子进程
        std::cout << "Uasge: " << argv[0] << " process_num" << std::endl;
        exit(USAGE_ERROR);
    }

    // 得到的字符串,可是我们需要整数,怎么将字符串转化成整数,使用函数stoi
    int num = std::stoi(argv[1]); // 代表 process_num

    // 保存每次创建子进程的pid值
    std::vector<pid_t> vid;

    // 将创建子进程封装成函数
    CreateChileProcess(num, &vid);
   
    // 父进程
    // 将父进程等待子进程封装成一个函数
    WaitChildProcess(vid);

    return right;
}

运行结果:

让子进程完成不同的任务,使用函数指针。示例代码:

// 函数指针
typedef void (*call_back)();

enum 
{
  right,
  UASGE_ERROR
};

// 子进程要执行的任务
void Task()
{
  int count = 5;
  while(count--)
  {
    printf("I am a child process, pid = %d, ppid = %d, count: %d\n", getpid(), getppid(), count);
    sleep(2);
  }
}

// 习惯规定:
// 输入: 参数使用 const &
// 输出: 参数使用 *
// 输入输出: 参数使用 &
void CreateChileProcess(int num, std::vector<pid_t>* vid, call_back cb)
{
    // 创建一批子进程
    for(int i = 0; i < num; i++)
    {
        pid_t id = fork();
   
        if(id == 0)
        {
            // 子进程
            cb();   // 子进程要执行的任务
            exit(0);
        }

        // vector容器存储子进程的pid值
        vid.push_back(id);
    }
}

void WaitChildProcess(const std::vector<pid_t>& vid)
{  
    // 等待子进程
    for(auto& pid : vid)
    { 
        // 父进程如何知道自己要等多少个子进程?
        // 使用 vector 容器,将每次创建的子进程的pid值存储起来
        // 父进程
        int status = 0;
        pid_t wid = waitpid(-1, &status, 0);
        if(wid > 0)
        {
            printf("子进程: %d 退出,wait_code: %d\n", wid, WEXITSTATUS(status));
            sleep(2);
        }
    }
}

int main(int argc, char* argv[])
{ 
    if(argc != 2)
    {
        // 指令的输入格式,要创建5个子进程
        std::cout << "Uasge: " << argv[0] << " process_num" << std::endl;
        exit(USAGE_ERROR);
    }

    // 得到的字符串,可是我们需要整数,怎么将字符串转化成整数,使用函数stoi
    int num = std::stoi(argv[1]); // 代表 process_num

    // 保存每次创建子进程的pid值
    std::vector<pid_t> vid;

    // 将创建子进程封装成函数
    CreateChileProcess(num, &vid, Task);
   
    // 父进程
    // 将父进程等待子进程封装成一个函数
    WaitChildProcess(vid);

    return right;
}

进程的程序替换

fork() 创建子进程之后,父子进程各自执行父进程代码的一部分,如果子进程就想执行一个全新的程序呢?进程的程序替换来完成这个功能!

先直接观察进程程序替换的现象。

示例代码:没有程序替换

printf("I am a process,pid: %d\n", getpid());

printf("正在执行自己的代码!\n");
printf("正在执行自己的代码!\n");
printf("正在执行自己的代码!\n");
printf("正在执行自己的代码!\n");
printf("正在执行自己的代码!\n");

运行结果:

若想要进程执行另一程序的代码,需要使用 exec* 函数,先使用之后再详细介绍。

示例代码:使用程序替换函数

printf("I am a process,pid: %d\n", getpid());

execl("/usr/bin/ls", "-a", "-l", NULL);

printf("正在执行自己的代码!\n");
printf("正在执行自己的代码!\n");
printf("正在执行自己的代码!\n");
printf("正在执行自己的代码!\n");
printf("正在执行自己的代码!\n");

运行结果:

一个进程执行另一个程序的代码,我们将这种现象称作程序替换为什么使用程序替换函数之后,后面的代码就不执行了呢?因为代码被替换了


程序替换的原理

将新的代码和数据加载到内存中,覆盖旧的代码段和数据段,这就叫做程序替换。程序替换是否创建了新的进程?没有,仅仅只是用新的代码和数据覆盖了旧的的代码和数据,进程的PCB并没有改变,没有创建新的PCB。程序替换的本质就是将磁盘的代码和数据加载到内存里。之前说过程序运行之前,必须先需要加载到内存,为什么?冯诺依曼体系结构规定。怎么做到的?OS提供对应的系统调用来完成这个加载过程,将磁盘的代码和数据加载到内存里,而这个系统调用就是程序替换函数exec*

在父子进程中使用程序替换函数,示例代码:

printf("I am a process,pid: %d\n", getpid());

pid_t id = fork();
if(id == 0)
{
    // 执行另一个程序的代码
    execl("/usr/bin/ls", "-a", "-l", NULL);
    exit(0);
}

// 父进程
wait(NULL);
printf("正在执行自己的代码!\n");
printf("正在执行自己的代码!\n");
printf("正在执行自己的代码!\n");
printf("正在执行自己的代码!\n");
printf("正在执行自己的代码!\n");

运行结果:

为什么会是这样的结果?父子进程发生程序替换之后,各自都有了独立代码和数据,父子进程彻底分离。

bash 命令行解释器中调用了 exec* 函数,bash 创建子进程,将执行的指令传递给子进程的 exec* 函数,如此就能执行用户输入的指令。


程序替换函数

程序替换函数共有七个:

#include <unistd.h>

extern char **environ;

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 *file, char *const argv[ ]);

int execvpe(const char *file, char *const argv[ ], char *const envp[ ]);

int execve(const char *path, char *const argv[ ], char *const envp[ ]);

记忆规律:

l(list):表示参数采用列表

v (vector):表示参数用数组

p(path):有 p 自动搜索环境变量

e(environment):表示自己维护环境变量

执行任何程序的步骤:

找到它,怎么找?通过路径+程序名

加载它

执行它,怎么执行?根据程序和选项决定


execl 函数

函数原型:int execl(const char *path, const char *arg, ...);

参数 path 表示要执行的程序在哪里,路径+程序名;参数… 是可变参数,如 printf 函数声明中的…,int printf(const char *format, ...),给一个函数传多个参数在命令行上怎么写,可变参数就怎么写。如要执行 ls -a -l 命令,函数写成 execl(“/usr/bin/ls”, “ls”, “-a”, “-l”, NULL),需要注意的是可变参数的最后一个参数必须是NULL,所有的程序替换函数的可变参数的最后一个参数必须是NULL

execl(“/usr/bin/ls”, “ls”, “-a”, “-l”, NULL)中的“/usr/bin/ls”表明执行谁,后面的参数表明怎么执行。程序替换函数的部分参数可以省略,但是不建议,有些函数的参数可以省略,有些函数的参数不可以省略,增加自己的记忆负担。

所有的 exec* 函数替换成功时都没有返回值,如果替换失败会返回-1。什么时候会替换失败?不需要接收返回值判断是否等于-1,只需要在函数的后面加上 exit 函数,如果替换成功,exit 函数是不会执行的,如果替换失败,就会执行 exit 函数,可以接收到退出码。

示例代码:

printf("I am a process,pid: %d\n", getpid());

pid_t id = fork();
if(id == 0)
{
    // 执行另一个程序的代码
    execl("/usr/bin/XXXX", "ls", "-a", "-l", NULL);
    exit(1);
}

int status = 0;
pid_t wid = waitpid(id, &status, 0);
if(wid > 0)
{
    printf("wait success, exit_code: %d\n", WEXITSTATUS(status));
}

示例结果:


execlp 函数

函数原型:int execlp(const char *file, const char *arg, ...);

execlp 函数不需要传路径,只需要传程序名,因为 execlp 会自动到环境变量 PATH 所显示的系统路径下去寻找。如要执行 ls -a -l 命令,函数写成 execlp(“ls”, “ls”, “-a”, “-l”, NULL)。

示例代码:

printf("I am a process,pid: %d\n", getpid());

pid_t id = fork();
if(id == 0)
{
    // 执行另一个程序的代码
    execlp("ls", "ls", "-a", "-l", NULL);
    exit(1);
}

int status = 0;
pid_t wid = waitpid(id, &status, 0);
if(wid > 0)
{
    printf("wait success, exit_code: %d\n", WEXITSTATUS(status));
}

运行结果:

函数 execlp(“ls”, “ls”, “-a”, “-l”, NULL) 中的”ls”重复吗?不重复,第一个 ls 是程序名,表明需要执行谁,第二个 ls 是可变参数,表明需要怎么执行。尽管删除其中一个 ls 之后,运行结果仍然是正确的,但是不推荐省略。


execv 函数

函数原型:int execv(const char *path, char *const argv[ ]);

参数 argv 是指针数组,参数 path 表示要执行谁,参数 argv 表示怎么执行。可以直接在外构建一个函数参数表,构建好之后,再传给 execv 函数。如要执行 ls -a -l 命令,先定义一个参数表char* myargv[ ] = { “ls”, “-a”, “-l”, NULL },函数写成 execv(“/usr/bin/ls”, myargv)。

示例代码:

printf("I am a process,pid: %d\n", getpid());

pid_t id = fork();
if(id == 0)
{
    // 执行另一个程序的代码
    char* argv[] = { "ls", "-a", "-l", NULL };
    execv("/usr/bin/ls", argv);
    exit(1);
}

int status = 0;
pid_t wid = waitpid(id, &status, 0);
if(wid > 0)
{
    printf("wait success, exit_code: %d\n", WEXITSTATUS(status));
}

运行结果:

传递的参数myargv,最终传递给谁了?传递给 ls 命令中的 main 函数参数 argv


execvp 函数

函数原型:int execvp(const char *file, char *const argv[ ]);

execvp函数不需要传路径,只需要传程序名,因为 execvp 会自动到环境变量 PATH 所显示的系统路径下去寻找。如要执行 ls -a -l 命令,先定义一个参数表char* argv[ ] = { “ls”, “-a”, “-l”, NULL },函数写成 execv(“ls”, argv),也可以写成 execv(argv[0], argv)。

示例代码:

printf("I am a process,pid: %d\n", getpid());

pid_t id = fork();
if(id == 0)
{
    // 执行另一个程序的代码
    char* argv[] = { "ls", "-a", "-l", NULL };
    execvp(argv[0], argv);
    exit(1);
}

int status = 0;
pid_t wid = waitpid(id, &status, 0);
if(wid > 0)
{
    printf("wait success, exit_code: %d\n", WEXITSTATUS(status));
}

运行结果:


上述演示的都是替换系统的命令,那么程序替换函数是否可以替换用户自己写的程序?当然可以。

先自己写一个程序 test,代码:

printf("这是用户自己编写的程序!\n");

示例代码:

printf("I am a process,pid: %d\n", getpid());

pid_t id = fork();
if(id == 0)
{
    // 执行另一个程序的代码
    execl("./test", "test", NULL);
    exit(1);
}

int status = 0;
pid_t wid = waitpid(id, &status, 0);
if(wid > 0)
{
    printf("wait success, exit_code: %d\n", WEXITSTATUS(status));
}

运行结果:

需要注意的是替换函数替换的都是二进制可执行程序,而不是源文件。此外也可以自己替换自己,但是会出现死循环。程序替换不是语言概念,而是系统概念。


execvpe 函数

函数原型:int execvpe(const char *file, char *const argv[ ], char *const envp[ ]);允许传入新的环境变量。

替换用户自己编写的程序,代码为:

int main(int argc, char* argv[], char* env[])
{
  for(int i = 0; i < argc; i++)
  {
    printf("argv[%d]: %s\n", i, argv[i]);
  }

  printf("\n");

  for(int i = 0; env[i]; i++)
  {
    printf("env[%d]: %s\n", i, env[i]);
  }
  
  printf("这是用户自己编写的程序!\n");

  return 0;
}

示例代码:

printf("I am a process,pid: %d\n", getpid());

pid_t id = fork();
if(id == 0)
{
    // 执行另一个程序的代码
    char* argv[] = { "./test", "-a", "-b", NULL };
    char* env[] = { "PATH=/home/zs/Linux/lesson19", NULL };

    execvpe("test", argv, env);
    exit(1);
}

int status = 0;
pid_t wid = waitpid(id, &status, 0);
if(wid > 0)
{
    printf("wait success, exit_code: %d\n", WEXITSTATUS(status));
}

运行结果:

为什么替换失败了,函数名带 p,表明函数自动到环境变量 PATH 所显示的系统路径下去寻找,那么 test 程序的工作路径在系统路径下吗?不存在,所以找不到 test 程序,可以在 execvpe 函数中显示test路径,即./test。

示例代码:

printf("I am a process,pid: %d\n", getpid());

pid_t id = fork();
if(id == 0)
{
    // 执行另一个程序的代码
    char* argv[] = { "./test", "-a", "-b", NULL };
    char* env[] = { "PATH=/home/zs/Linux/lesson19", NULL };
    execvpe("./test", argv, env); // 覆盖式的使用全新的环境变量表
    exit(1);
}

int status = 0;
pid_t wid = waitpid(id, &status, 0);
if(wid > 0)
{
    printf("wait success, exit_code: %d\n", WEXITSTATUS(status));
}

如果不想自己新建环境变量表,可以使用系统默认的,即传参 environ。

printf("I am a process,pid: %d\n", getpid());

pid_t id = fork();
if(id == 0)
{
    // 执行另一个程序的代码
    char* argv[] = { "./test", "-a", "-b", NULL };
    char* env[] = { "PATH=/home/zs/Linux/lesson19", NULL };
    extern char** environ;
    execvpe("./test", argv, environ); // 覆盖式的使用全新的环境变量表
    exit(1);
}

int status = 0;
pid_t wid = waitpid(id, &status, 0);
if(wid > 0)
{
    printf("wait success, exit_code: %d\n", WEXITSTATUS(status));
}

运行结果:

由此可以知道,argv 和 env 传递给了 test 程序的 main 函数参数 argv 和 env,如果不想自己新建环境变量表,可以传系统默认的环境变量表,即 environ。我们自己传递的 env 参数,是对系统环境变量做的覆盖,如果想要在系统环境变量的基础上新增环境变量应该怎么做?使用 putenv 函数,功能:新增环境变量。子进程也可以选择不继承父进程的环境变量,用户可以自己实现一个环境变量表,供子进程使用。子进程的命令行参数表和环境变量表都是父进程通过 exec* 函数传递的。


在我们使用 man 指令查 exec* 函数时,只显示了六个函数,并没有 execve 函数。

execve 函数是单独的:

这是因为 execve 函数是系统调用,其余的 exec* 函数是库函数,它们都调用 execve 函数。这些程序替换函数的核心作用就是将磁盘上的代码和数据加载到内存中。


知识回顾

聊聊Linux中的“进程”到底是什么?-CSDN博客

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐