• Linux 下进程管理 API 之创建、查询、退出和收回详解


    引用:之前的文章介绍了 Linux 下进程相关的基础概念和一些常见的 shell 命令,本文则延续之前的思路——继续 Linux 下进程知识的学习与总结,介绍 Linux 下进程管理相关的 API,包括进程创建、进程信息查询、进程退出和进程回收。

    一、进程信息查询

    进程常用信息介绍

    每个进程都由一个进程号来标识,其类型为 pid_t,进程号的类型—— pid_t 其实为一个短整形,所以pid_t能表示的范围是:0~32767。进程号总是唯一的,但进程号可以重用:同一时刻,只能有一个进程使用一个进程号,当一个进程终止后,该进程号就可以再次被其他进程使用。
    在这里插入图片描述

    接下来,再给大家介绍三个不同的进程号。

    • 进程号(PID):标识进程的一个非负整型数。
    • 父进程号(PPID):任何进程( 除 init 进程)都是由另一个进程创建,该进程称为被创建进程的父进程,对应的进程号称为父进程号(PPID)。例如A 进程创建了 B 进程,A 的进程号就是 B 进程的父进程号。
    • 进程组号(PGID):进程组是一个或多个进程的集合。他们之间相互关联,进程组可以接收同一终端的各种信号,关联的进程有一个进程组号(PGID) 。这个过程有点类似于 QQ 群,组相当于 QQ 群,各个进程相当于各个好友,把各个好友都拉入这个 QQ 群里,主要是方便管理,特别是通知某些事时,只要在群里吼一声,所有人都收到,简单粗暴。但是,这个进程组号和 QQ 群号是有点区别的,默认的情况下,当前的进程号会当做当前的进程组号

    进程信息查询 API

    (1)getpid 函数

    #include 
    #include 
    
    pid_t getpid(void);
    功能:
        获取本进程号(PID)
    参数:
        无
    返回值:
        本进程号(注意:该函数总是执行成功,所以返回值不需要进行错误检测)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    (2)getppid函数

    #include 
    #include 
    
    pid_t getppid(void);
    功能:
        获取调用此函数的进程的父进程号(PPID)
    参数:
        无
    返回值:
        调用此函数的进程的父进程号(PPID)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    (3)getpgid函数

    #include 
    #include 
    
    pid_t getpgid(pid_t pid);
    功能:
        获取进程组号(PGID)
    参数:
        pid:进程号
    返回值:
        参数为 0 时返回当前进程组号,否则返回参数指定的进程的进程组号
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    示例程序

    // test.c
    #include 
    #include 
    #include 
    
    // 获取进程号、父进程号、进程组号
    int main()
    {
        pid_t pid, ppid, pgid;
        pid = getpid();
        printf("pid = %d\n", pid);
        ppid = getppid();
        printf("ppid = %d\n", ppid);
        pgid = getpgid(pid);
        printf("pgid = %d\n", pgid);
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    yxm@192:~$ gcc test.c -o test
    yxm@192:~$ ./test 
    pid = 27095
    ppid = 26906
    pgid = 27095
    
    • 1
    • 2
    • 3
    • 4
    • 5

    二、进程创建

    系统允许一个进程创建新进程,新进程即为子进程,子进程还可以创建新的子进程,形成进程树结构模型。

    进程创建 API

    #include 
    #include 
    
    pid_t fork(void);	
    功能:
        用于从一个已存在的进程中创建一个新进程,新进程称为子进程,原进程称为父进程。
    参数:
        无
    返回值:
        成功:本函数返回值会返回两次,一次是子进程中返回 0,一次是父进程中返回子进程 ID。
        失败:父进程中返回-1。失败的两个主要原因是:
            1)当前的进程数已经达到了系统规定的上限,这时 errno 的值被设置为 EAGAIN。
            2)系统内存不足,这时 errno 的值被设置为 ENOMEM。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    示例代码:

    #include 
    #include 
    #include 
    
    int main()
    {
        fork();
        printf("id ==== %d\n", getpid());   // 获取进程号 
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    运行结果如下:

    yxm@192:~$ gcc test.c -o test
    yxm@192:~$ ./test 
    id ==== 27332					# 父进程的进程号
    id ==== 27333					# 子进程的进程号
    
    • 1
    • 2
    • 3
    • 4

    从运行结果,我们可以看出,fork() 之后的打印函数打印了两次,而且打印了两个进程号,这说明,fork() 之后确实创建了一个新的进程,新进程为子进程,原来的进程为父进程。

    父子进程关系

    上一小节中,我们使用 fork() 函数得到的子进程实际上是父进程的一个复制品,它从父进程处继承了整个进程的地址空间:包括进程上下文(进程执行活动全过程的静态描述)、进程堆栈、打开的文件描述符、信号控制设定、进程优先级、进程组号等。

    子进程所独有的只有它的进程号,计时器等(只有小量信息)。因此,使用 fork() 函数的代价是很大的。

    在这里插入图片描述

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

    • 实际上,更准确来说,Linux 的 fork() 使用是通过写时拷贝 (copy- on-write) 技术来实现克隆自己的。写时拷贝是一种可以推迟甚至避免拷贝数据的技术。内核一开始并不复制整个进程的地址空间,而是让父子进程共享同一个地址空间。只有在需要写入的时候才会复制地址空间,从而使各个进行拥有各自的地址空间。也就是说,资源的复制是在需要写入的时候才会进行,在此之前,只有以只读方式共享——写时拷贝,读时共享

    • 【补充】fork之后父子进程共享文件,fork产生的子进程与父进程相同的文件文件描述符指向相同的文件表,引用计数增加,共享文件文件偏移指针。

      在这里插入图片描述
      在这里插入图片描述

    区分父子进程

    子进程是父进程的一个复制品,可以简单认为父子进程的代码一样的。

    那大家想过没有,这样的话,父进程做了什么事情,子进程也做什么事情(如上面的例子),是不是不能实现满足我们实现多任务的要求呀(多任务一般是父进程做一件事,子进程做另外一件事,从而实现并发)?

    实际上可以通过 fork() 的返回值区别父子进程:fork() 函数被调用一次,但返回两次。两次返回的区别是:子进程的返回值是 0,而父进程的返回值则是新子进程的进程 ID

    测试程序如下

    #include 
    #include 
    #include 
    
    int main()
    {
        pid_t pid;
        pid = fork();
        if (pid < 0)
        {   // 没有创建成功  
            perror("fork");
            return 0;
        }
    
        if (0 == pid)
        { // 子进程  
            while (1)
            {
                printf("I am son\n");
                sleep(1);
            }
        }
        else if (pid > 0)
        { // 父进程  
            while (1)
            {
                printf("I am father\n");
                sleep(1);
            }
        }
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    #include 
    #include 
    #include 
    
    int main()
    {
        pid_t pid;
        pid = fork();
        if (pid < 0)
        {   // 没有创建成功  
            perror("fork");
            return 0;
        }
    
        if (0 == pid)
        { // 子进程  
            while (1)
            {
                printf("I am son\n");
                sleep(1);
            }
        }
        else if (pid > 0)
        { // 父进程  
            while (1)
            {
                printf("I am father\n");
                sleep(1);
            }
        }
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    yxm@192:~$ gcc test.c -o test
    yxm@192:~$ ./test 
    I am father
    I am son
    I am father
    I am son
    ...
    ^C
    yxm@192:~$ 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    运行结果如下:通过运行结果,可以看到,父子进程各做一件事(各自打印一句话)。这里,我们只是看到只有一份代码,实际上,fork() 以后,有两个地址空间在独立运行着,有点类似于有两个独立的程序(父子进程)在运行。

    • 一般来说,在 fork() 之后是父进程先执行还是子进程先执行是不确定的。这取决于内核所使用的调度算法

    • 需要注意的是,在子进程的地址空间里,子进程是从 fork() 这个函数后才开始执行代码
      在这里插入图片描述

    • 父子进程地址空间:父子进程各自的地址空间是独立的,也包括堆区、栈区和全局数据区。只不过在运行的时候,会根据 fork() 函数的返回值不一样而调用不同的代码段。

    • 【注意】子进程还可以创建新的子进程,形成进程树结构模型:因为子进程是从 fork() 这个函数后才开始执行代码,所以在连续创建子进程或者循环创建子循环时候需要小心,否则创建的子进程数量可能远远超过预想的子进程数量,如下所示:

      #include 
      #include 
      // 想要连续创建两个进程
      int main() 
      {
          pid_t pid;
      	// 连续创建两个子进程
          fork();
          fork();
         
          return 0; 
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12

      上例中,想要连续创建两个子进程,但实际上创建了三个子进程,因为第一个 fork() 之后创建了一个子进程,而该子进程从第一个 fork() 函数后才开始执行代码,所以会执行第二个 fork() 创建了一个孙进程,再加上父进程执行第二个 fork() 函数又会创建新的子进程,一共创建了三个进程

    三、进程退出

    既然可以创建进程,那如何结束一个进程呢?可以使用一下 API 函数:

    #include 
    void exit(int status);
    
    #include 
    void _exit(int status);
    功能:
        结束调用此函数的进程。
    参数:
        status:进程退出时的一个状态信息,父进程回收子进程资源的时候可以获取到(在父子进程中,如果子进程退出		了,exit 就能得到子进程退出的状态)。
    返回值:
        无
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    exit() 和 exit() 函数功能和用法是一样的,但还是有两点区别:

    • 使用时,所包含的头文件不一样;
    • exit() 属于标准库函数(标准 c 库中的函数),_exit() 属于系统调用函数(linux 系统中的函数)。由于 exit() 底层会调用 _exit() 函数,其在调用_exit() 函数之前,会做一些安全处理,所以使用 exit() 相对于直接使用 _exit() 更加安全

    在这里插入图片描述

    exit()示例

    #include 
    #include 
    
    int main() 
    {
        printf("hello\n");
        printf("world");
        exit(0);    		// 等价于return 0;
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    yxm@192:~$ gcc test.c -o test
    yxm@192:~$ ./test 
    hello						
    worldyxm@192:~$ 
    
    • 1
    • 2
    • 3
    • 4

    _exit()示例

    #include 
    #include 
    
    int main() 
    {
        printf("hello\n");
        printf("world");
        _exit(0);
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    worldyxm@192:~$ gcc test.c -o test
    yxm@192:~$ ./test 
    hello
    yxm@192:~$ 
    
    • 1
    • 2
    • 3
    • 4

    exit()_exit() 的运行结果如下:

    • 第一个 printf() 中有 ‘\n’ 换行,带上换行之后,printf() 内部会自动实现刷新缓冲区的功能,所以 exit()示例与 _exit()示例中的 ‘hello’ 都被打印出来了;
    • 第二个 printf() 中没有 ‘\n’ 换行,且 _exit() 的内部没有刷新缓冲区,所以 _exit()示例中的 ‘world’ 遗留在缓冲区,在还没来得及输出到标准输出文件(stdout)的情况系,程序就已经结束, ‘world’ 没有来得及打印。

    四、进程回收

    进程回收概述

    在每个进程退出的时候,内核释放该进程所有的资源、包括打开的文件、占用的内存等。但是仍然为其保留一定的信息,这些信息主要是进程控制块 PCB 的信息(包括进程号、退出状态、运行时间等)。

    父进程可以通过调用 wait()waitpid() 等到它的子进程退出状态同时彻底清除掉这个进程。所以子进程运行结束或者进程退出时,父进程有义务回收子进程的资源

    wait()waitpid() 函数的功能一样,区别在于,wait() 函数会阻塞,waitpid() 可以设置不阻塞,waitpid() 还可以指定等待哪个子进程结束,下文会详细介绍。

    注意:一次wait或waitpid调用只能清理一个子进程,清理多个子进程应使用循环。

    进程回收 API

    (1)wait函数

    #include 
    #include 
    
    pid_t wait(int *status);
    功能:
        等待任意一个子进程结束,如果任意一个子进程结束了,此函数会回收该子进程的资源。
    参数:
        status : 进程退出时的状态信息,本参数是传出参数。
    返回值:
        成功:已经结束子进程的进程号
        失败: 返回 -1,失败的原因:
            1、没有任何有;
            2、所有的子进程都已结束;
            3、函数调用函数失败。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 调用 wait() 函数的进程会挂起(阻塞),直到它的一个子进程退出或收到一个不能被忽视的信号时才被唤醒(相当于继续往下执行)。

    • 若调用进程没有子进程,函数立刻返回,返回-1;若它的子进程已经结束,也会立即返回,返回-1。

    • 如果参数 status 的值不是 NULL,wait() 就会把子进程退出时的状态取出并存入其中,这是一个整数值(int),指出了子进程是正常退出还是被非正常结束的。这个退出信息在一个 int 中包含了多个字段,直接使用这个值是没有意义的,我们需要用宏定义取出其中的每个字段。退出信息相关宏函数可分为如下三组:

      1. WIFEXITED(status) 为非0 → 进程正常结束

        WEXITSTATUS(status) 如上宏为真,使用此宏 → 获取进程退出状态

      2. WIFSIGNALED(status) 为非0 → 进程异常终止

        WTERMSIG(status) 如上宏为真,使用此宏 → 取得使进程终止的那个信号的编号。

      3. WIFSTOPPED(status) 为非0 → 进程处于暂停状态

        WSTOPSIG(status) 如上宏为真,使用此宏 → 取得使进程暂停的那个信号的编号。

        WIFCONTINUED(status) 如上宏为真 → 进程暂停后已经继续运行

    示例

    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    int main()
    {
       int status = 0;
       int i = 0;
       int ret = -1;
       pid_t pid = -1;
       
       // 创建子进程
       pid = fork();
       if (pid < 0)  // 没有创建成功  
       {   
           perror("fork");
           return -1;
       }
    
       if (0 == pid)   // 子进程  
       {
           for (i =0; i < 5; i++)
           {
               printf("child process do thing %d\n", i + 1);
               sleep(1);
           }
         
           exit(10);   //子进程终止
       }
    
       // 父进程执行
       printf("父进程等待子进程退出,回收其资源\n");
       ret = wait(&status); // 父进程在此处会阻塞,等待子进程退出,返回值为exit函数的参数
       if (-1 == ret)
       {
           perror("wait");
           return -1;
       }
       printf("父进程回收了子进程资源\n");
       
      
       if (WIFEXITED(status))   //属于正常退出
       {
       	printf("子进程退出状态码:%d\n", WEXITSTATUS(status));
       }
       else if (WIFSIGNALED(status))  //属于异常终止退出
       {
       	printf("子进程被信号%d杀死了...\n", WTERMSIG(status));
       }
       else if (WIFSTOPPED (status))  //属于进程暂停
       {
           printf("子进程被信号%d暂停...\n", WSTOPSIG(status ) );
       }
       return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    yxm@192:~$ gcc test.c -o test
    yxm@192:~$ ./test 
    父进程等待子进程退出,回收其资源
    child process do thing 1
    child process do thing 2
    child process do thing 3
    child process do thing 4
    child process do thing 5
    父进程回收了子进程资源
    子进程退出状态码:10
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    (2)waitpid函数

    #include 
    #include 
    
    pid_t waitpid(pid_t pid, int *status, int options);
    功能:
        等待指定进程号终止,如果子进程终止了,此函数会回收子进程的资源,可以设置是否阻塞
    
    参数:
        pid : 参数 pid 的值有以下几种类型:
          pid > 0  某个子进程的进程号,相当于等待并回收指定子进程。
          pid = 0  等待并回收同一个进程组中的任何子进程,如果子进程已加入了别的进程组,waitpid 不会等待它。
          pid = -1 等待并回收任一子进程,此时 waitpid 和 wait 作用一样(最常用)。
          pid < -1 等待并回收指定进程组中的任何子进程,这个进程组的 ID 等于 pid 的绝对值。
    
        status : 进程退出时的状态信息。和 wait() 用法一样。
    
        options : options 提供了一些额外的选项来控制 waitpid()。
                0:		同 wait() 一样,阻塞父进程,等待子进程退出。
                WNOHANG:非阻塞。
                WUNTRACED:如果子进程暂停了则此函数马上返回,并且不予以理会子进程的结束状态。
            		(由于涉及到一些跟踪调试方面的知识,加之极少用到)
                     
    返回值: waitpid() 的返回值比 wait() 稍微复杂一些,一共有 3 种情况:
            1) 当正常返回的时候,waitpid() 返回收集到的已经回收子进程的进程号,则返回 > 0;
            2) 如果设置了选项 WNOHANG,而调用中 waitpid() 发现没有已退出的子进程可等待,则返回 0;
            3) 如果调用中出错,则返回-1,这时 errno 会被设置成相应的值以指示错误所在,
            	如:当 pid 所对应的子进程不存在,或此进程存在,但不是调用进程的子进程, 
            	waitpid() 就会出错返回,这时 errno 被设置为 ECHILD;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    示例

    #include 
    #include 
    #include 
    #include 
    #include 
    
    int main() {
    
        // 有一个父进程,创建5个子进程(兄弟)
        pid_t pid;
    
        // 创建5个子进程
        for(int i = 0; i < 5; i++) 
        {
            pid = fork();
            if(pid == 0) 		// 子进程还会创建新的子进程,所以需要判断语句以保证只会创建 5 个子进程
            {
                break;
            }
        }
    
        if(pid > 0) 
        {
            // 父进程
            while(1) 
            {
                printf("parent, pid = %d\n", getpid());
                sleep(1);
    
                int st;
                // int ret = waitpid(-1, &st, 0);
                int ret = waitpid(-1, &st, WNOHANG);
    
                if(ret == -1) 
                {
                    break;
                } else if(ret == 0) 
                {
                    // 说明还有子进程存在
                    continue;
                } else if(ret > 0) 
                {
                    if(WIFEXITED(st)) 
                    {
                        // 是不是正常退出
                        printf("退出的状态码:%d\n", WEXITSTATUS(st));
                    }
                    if(WIFSIGNALED(st)) 
                    {
                        // 是不是异常终止
                        printf("被哪个信号干掉了:%d\n", WTERMSIG(st));
                    }
                    printf("child die, pid = %d\n", ret);
                }       
            }
        } 
        else if (pid == 0)
        {
            // 子进程
             while(1) 
             {
                printf("child, pid = %d\n",getpid());    
                sleep(1);       
             }
            exit(0);
        }
        return 0; 
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68

    推荐一个零声学院免费公开课程,个人觉得老师讲得不错,
    分享给大家:[Linux,Nginx,ZeroMQ,MySQL,Redis,
    fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,
    TCP/IP,协程,DPDK等技术内容,点击立即学习:服务器课程

    总结

    本文介绍一些进程管理方面的知识点,包括进程信息的查询、进程的创建、进程的退出和进程资源回收的回收,同时还补充了一些相关的实例,篇幅不长,但实际上内容更多是 API 的使用总结,可以起到查缺补漏和知识字典的作用。

  • 相关阅读:
    数据结构与算法_排序算法_冒泡排序和选择排序
    Java - 位运算的基本原理和用途
    ORA_00604
    #QT(网络文件下载)
    项目管理之项目计划TG1
    数据结构栈的使用——马踏棋盘
    【业务场景】用户连点
    源码编译net-snmp
    TCPIP网络编程基础知识
    Dubbo详解,用心看这一篇文章就够了【重点】
  • 原文地址:https://blog.csdn.net/weixin_45004203/article/details/126037125