栏目分类:
子分类:
返回
文库吧用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
文库吧 > IT > 软件开发 > 后端开发 > Java

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

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

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

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

文章目录
    • 一、进程信息查询
      • 进程常用信息介绍
      • 进程信息查询 API
    • 二、进程创建
      • 进程创建 API
      • 父子进程关系
      • 区分父子进程
    • 三、进程退出
    • 四、进程回收
      • 进程回收概述
      • 进程回收 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)
参数:
    无
返回值:
    本进程号(注意:该函数总是执行成功,所以返回值不需要进行错误检测)

(2)getppid函数

#include 
#include 

pid_t getppid(void);
功能:
    获取调用此函数的进程的父进程号(PPID)
参数:
    无
返回值:
    调用此函数的进程的父进程号(PPID)

(3)getpgid函数

#include 
#include 

pid_t getpgid(pid_t pid);
功能:
    获取进程组号(PGID)
参数:
    pid:进程号
返回值:
    参数为 0 时返回当前进程组号,否则返回参数指定的进程的进程组号

示例程序:

// test.c
#include 
#include 
#include 

// 获取进程号、父进程号、进程组号
int main()
{
    pid_t pid, ppid, pgid;
    pid = getpid();
    printf("pid = %dn", pid);
    ppid = getppid();
    printf("ppid = %dn", ppid);
    pgid = getpgid(pid);
    printf("pgid = %dn", pgid);
    return 0;
}
yxm@192:~$ gcc test.c -o test
yxm@192:~$ ./test 
pid = 27095
ppid = 26906
pgid = 27095
二、进程创建

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

进程创建 API
#include 
#include 

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

示例代码:

#include 
#include 
#include 

int main()
{
    fork();
    printf("id ==== %dn", getpid());   // 获取进程号 
    return 0;
}

运行结果如下:

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

从运行结果,我们可以看出,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 sonn");
            sleep(1);
        }
    }
    else if (pid > 0)
    { // 父进程  
        while (1)
        {
            printf("I am fathern");
            sleep(1);
        }
    }

    return 0;
}
#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 sonn");
            sleep(1);
        }
    }
    else if (pid > 0)
    { // 父进程  
        while (1)
        {
            printf("I am fathern");
            sleep(1);
        }
    }

    return 0;
}
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:~$ 

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

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

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

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

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

    #include 
    #include 
    // 想要连续创建两个进程
    int main() 
    {
        pid_t pid;
    	// 连续创建两个子进程
        fork();
        fork();
       
        return 0; 
    }
    

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

三、进程退出

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

#include 
void exit(int status);

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

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

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

exit()示例:

#include 
#include 

int main() 
{
    printf("hellon");
    printf("world");
    exit(0);    		// 等价于return 0;
    return 0;
}
yxm@192:~$ gcc test.c -o test
yxm@192:~$ ./test 
hello						
worldyxm@192:~$ 

_exit()示例:

#include 
#include 

int main() 
{
    printf("hellon");
    printf("world");
    _exit(0);
    return 0;
}
worldyxm@192:~$ gcc test.c -o test
yxm@192:~$ ./test 
hello
yxm@192:~$ 

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、函数调用函数失败。
  • 调用 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 %dn", 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("子进程退出状态码:%dn", WEXITSTATUS(status));
   }
   else if (WIFSIGNALED(status))  //属于异常终止退出
   {
   	printf("子进程被信号%d杀死了...n", WTERMSIG(status));
   }
   else if (WIFSTOPPED (status))  //属于进程暂停
   {
       printf("子进程被信号%d暂停...n", WSTOPSIG(status ) );
   }
   return 0;
}
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

(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;

示例:

#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 = %dn", 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("退出的状态码:%dn", WEXITSTATUS(st));
                }
                if(WIFSIGNALED(st)) 
                {
                    // 是不是异常终止
                    printf("被哪个信号干掉了:%dn", WTERMSIG(st));
                }
                printf("child die, pid = %dn", ret);
            }       
        }
    } 
    else if (pid == 0)
    {
        // 子进程
         while(1) 
         {
            printf("child, pid = %dn",getpid());    
            sleep(1);       
         }
        exit(0);
    }
    return 0; 
}

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

总结

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

转载请注明:文章转载自 www.wk8.com.cn
本文地址:https://www.wk8.com.cn/it/1032992.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 wk8.com.cn

ICP备案号:晋ICP备2021003244-6号