【Linux进程控制】二、进程控制——fork()系统调用深度刨析

2024-08-08 17:10:15 浏览数 (1)

1. fork()、getpid()、getppid()函数介绍

1.1 fork()函数介绍

fork()用于创建一个子进程,我们在shell下执行一个命令其实也是通过fork()实现的,fork()是Linux下最基本的一个系统调用。fork()最大的特点就是一次调用,两次返回,两次返回主要是区分父子进程,因为fork()之后将出现两个进程,所以有两个返回值,父进程返回子进程ID,子进程返回0。

  • 包含头文件
代码语言:javascript复制
#include <unistd.h>
  • 函数原型
代码语言:javascript复制
pid_t fork(void);
  • 函数功能 fork() creates a new process by duplicating the calling process. The new process, referred to as the child, the calling process, referred to as the parent. 通过复制的方式创建一个进程,被创建的进程称为子进程,调用进程称为父进程,复制的子进程是从父进程fork()调用后面的语句开始执行的。
  • 函数参数
    • void
  • 函数返回值
    • On success, the PID of the child process is returned in the parent, and 0 is returned in the child.
      • 父进程返回子进程ID
      • 子进程返回0
    • On failure, -1 is returned in the parent, no child process is created, and errno is set appropriately. 失败返回-1并设置errno。

1.2 getpid()函数与getppid()函数介绍

  • 包含头文件
代码语言:javascript复制
#include <sys/types.h>
#include <unistd.h>
  • 函数原型
代码语言:javascript复制
pid_t getpid(void);
pid_t getppid(void);
  • 函数功能
    • getpid() returns the process ID of the calling process. 获得当前进程的ID。
    • getppid() returns the process ID of the parent of the calling process. 获得当前进程的父进程的ID。
  • 函数参数 void
  • 函数返回值
    • getpid()返回当前进程ID
    • getppid()返回当前进程的父进程ID

2. fork()工作机制

2.1 fork()的实现机制——一次调用两次返回与进程复制

下面通过一个案例来分析fork()是如何创建进程,又是如何返回的。

代码语言:javascript复制
/************************************************************
  >File Name  : fork_test.c
  >Author     : Mindtechnist
  >Company    : Mindtechnist
  >Create Time: 2022年05月18日 星期三 15时59分29秒
************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>

int main(int argc, char* argv[])
{
    printf("=== process begin ===n");
    pid_t pid = fork();
    if(pid == -1)
    {
        perror("fork err");
        return -1;  
    }
    if(pid == 0) /*子进程*/
    {
        printf("i am child: %d, may parent: %dn", getpid(), getppid());    
        /*  test2
        while(1)
        {
            printf("fork processn");
            sleep(1);
        }
        */
    }
    if(pid > 0)
    {
        printf("i am call: %d, child: %d, parent: %dn", getpid(), pid, getppid()); 
        /*  test1
        sleep(1);
        */
        /*  test2
        while(1)
        {
            sleep(1);
        }
        */
    }
    printf("=== process end ===n");
    return 0;
}

编译运行该程序,我们会发现一个很有意思的现象

首先反常的第一点,我们在程序中的打印顺序是先进入子进程(pid == 0)分支,再进入父进程(pid > 0)分支,但实际的打印顺序是先执行了父进程分支的printf()函数,后执行的子进程分支到的printf()函数;第二点是,在执行子进程的printf()函数时,竟然已经回到了shell下,可以看图中高亮标出的位置。下面对着两点详细分析;第三点,子进程打印的父进程ID和父进程自己打印的ID不同。

我们已经知道,fork()系统调用的特点是一次调用两次返回,并且子进程的创建是对父进程的复制,那么是从哪复制开始复制的呢,我们根据程序运行结果分析,程序只打印了一次begin语句,说明不是从头开始复制的,实际上它是从fork()的下一句开始复制的,从fork()开始,后面就成了两个分支。

我们看到的运行结果中红色标记的①,实际上是由父进程打印的,②是由子进程打印的,既然不是一个进程打印的,那也就没有先后顺序的问题了。而子进程打印的父进程ID是1,父进程打印的自己的ID是5270,这是因为在子进程结束前,父进程就已经结束了,新建的子进程变成了孤儿进程,所以它会被1号进程收养,所以新建子进程的父进程ID是1,这也是为什么第二个printf()语句是在shell下执行的原因,因为原来的父进程结束了,所以回到了shell进程下,此时子进程还没有结束,它被1号进程接管,继续执行后面的语句,直到结束。

(实际上,这里的3397进程就是我们的shell进程,shell进程是我们自己启动的进程的父进程;而1号进程则是init进程,init进程是Linux下最原始的进程,是所有进程最终的父进程。)

我们可以在父进程中加一个sleep()函数(放开上面代码中test1注释掉的代码即可),让父进程等一下子进程,并看一下效果,这次就好了。

2.2 shell进程控制命令

下面我们通过shell下的进程控制命令进一步分析上面所讲的fork()实现机制,首先介绍几个命令:

  • ps 查看进程信息,主要用到下面两个参数
    • ps aux
    • ps ajx:可以查看父进程ID,追溯进程之间的关系
  • kill 给进程发送信号,通过这个命令可以杀死进程,常用的两个参数
    • kill -l:查看所有信号;
    • kill -9 pid:给进程号为pid的进程发送9号信号,杀死进程,实际上相当于 kill -SIGKILL pid,也可以直接通过 kill pid 来杀死pid进程;

我们再做一个测试,将上面代码中的test2处的注释放开,编译并运行程序,让两个进程一直在while中执行

开始循环后,我们另起一个shell来查看进程信息,可以通过管道和grep过滤我们需要的进程信息

通过ajx追溯进程血缘关系

可以看到fork()的调用进程5721,它的父进程是3397也就是 bash shell 进程

通过kill杀死父进程,可以看到子进程被1号进程接管

1号进程就是init进程

3. 进程创建的控制

3.1 控制进程创建个数

我们通过一个for循环来创建进程

代码语言:javascript复制
/************************************************************
  >File Name  : mutifork.c
  >Author     : Mindtechnist
  >Company    : Mindtechnist
  >Create Time: 2022年05月18日 星期三 19时33分50秒
************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>

int main(int argc, char* argv[])
{
    int i = 0;
    pid_t pid = 0;
    for(i = 0; i < 5; i  )
    {
        pid = fork();
        if(pid == 0)
        {
            printf("i am chiled: %d, ppid: %dn", getpid(), getppid());
            /*
            break;
            */
        }   
        if(pid > 0)
        {
            printf("i am call: %d, child:%d, ppid: %dn", getpid(), pid, getppid());
        }
    }
    while(1)
    {
        sleep(1);
    }
    return 0;
}

编译执行,在程序我们期望的是创建5个进程,但是实际运行后出现了一大堆进程,我们可以用wc命令统计一下

shell命令统计创建的进程个数

代码语言:javascript复制
ps aux | grep mutifork | grep -v grep | wc -l

总共有32个进程,我们在程序中只循环了5次,为什么有32个进程呢,下面看一张图

每次fork的时候,进程都会一分为二,所以5次循环相当于创建了2的5次方,也就是32个进程。要想避免这种情况,只需要根据返回值判断当前为子进程的时候就退出循环即可,也就是把上面代码中注释掉的break放开即可。

3.2 进程顺序控制

使用fork()创建的进程都是一样的,在操作系统看来没有区别,先后顺序也是不确定的,我们要想控制进程的退出顺序,需要自己去实现这个逻辑。比如说我们可以依据for循环中i的值来判断哪个进程先创建的,哪个进程后创建的,按照逻辑i小的应该是先创建的,因为C语言就是顺序执行的。因为子进程创建出来就break退出for循环了,所以五个子进程对应的i是0-4,而只有最开始的父进程可以执行到i=5。

代码语言:javascript复制
sleep(i); /*不同进程睡眠时间不同,第一个创建的进程
            i的值为0,睡眠最短,最先退出,后面的进
            程对应的i逐渐增大,睡眠时间增加,退出越晚*/
if(i < 5)
{
    printf("child: %d, parent: %dn", getpid(), getppid());
}
else
{
    printf("parent: %dn", getpid());
}

0 人点赞