Linux:进程控制(创建/终止/等待/获取退出信息/多进程)

上篇文章:Linux:进程虚拟地址空间|虚拟内存管理


目录

1.进程创建

1.1fork函数

1.2写时拷贝

1.3fork常规用法

1.4fork调用失败的原因

1.5实际运用

2.进程终止

2.1退出场景

退出信号举例:

2.2常见退出方法

2.2.1退出码

2.2.2strerror

2.2.3错误码和退出码区别

2.2.4进程正常退出

3.进程等待

3.1进程等待必要性

3.2进程等待的方法

3.2.1wait方法

3.2.2waitpid方法

3.2.3获取子进程status

3.2.4如何获取到子进程退出信息

3.2.5waitpid中的options

4.编写一个一次等待多个子进程的代码


1.进程创建

进程 = 内核数据结构(task_strct + mm_struct + vm_area_struct ...)(侧重进程管理)+ 代码和数据(侧重进程执行),其本质就是系统内多了一个进程。并且进程具有独立性,表现为内核结构独立,代码和数据独立。

1.1fork函数

在 linux 中 fork 函数是非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。

复制代码
#include <unistd.h>
pid_t fork(void);
返回值:⼦进程中返回0,⽗进程返回⼦进程id,出错返回-1

进程调用fork,当控制转移到内核中的fork代码后,内核会分配新的内存块和内核数据结构给子进程;将父进程部分数据结构内容拷贝到子进程;添加子进程到系统进程列表当中;fork返回,开始调度器调度。

当一个进程调用fork之后,就有两个二进制代码相同的进程。而且它们都运行到相同的地方,但每个进程都将开始它们各自的行为:

复制代码
int main( void )
{
     pid_t pid;
     
     printf("Before: pid is %d\n", getpid());
     if ( (pid=fork()) == -1 )perror("fork()"),exit(1);
     printf("After:pid is %d, fork return %d\n", getpid(), pid);
         sleep(1);
     return 0;
}
 
运⾏结果:
[root@localhost linux]# ./a.out
Before: pid is 43676
After:pid is 43676, fork return 43677
After:pid is 43677, fork return 0

上述代码中,进程43677没有打印before,原因是:

所以fork之前,父进程独立运行,fork之后,父子两个执行流分别执行,而fork之后谁先执行由调度器决定。

fork函数返回值,子进程返回0,父进程返回的是子进程的pid。

1.2写时拷贝

通常,父子代码共享,父子不再写入时,数据也是共享的,当任意一方试图写入时,便以写时拷贝的方式各自一份副本:

因为有写时拷贝的技术存在,所以父子进程得以彻底分离,完成了进程独立性的技术保证。写时拷贝是一种延时申请技术,可以提高整机内存的使用率。

1.3fork常规用法

1.一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如:父进程等待客户端请求,生成子进程来处理请求。

2.一个进程要执行一个不同的程序,例如子进程从fork返回后,调用exec函数。

1.4fork调用失败的原因

1.系统中有太多的进程

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

1.5实际运用

使用fork + 写时拷贝技术,进行安全备份。一般用在内存数据库中,进行数据持久化,也就是保存到磁盘当中。

复制代码
    1 #include <stdio.h>
    2 #include <unistd.h>
    3 #include <time.h>
E>  4 #include <stdlib.h>
    5 
    6 #define NUM 10
    7 
    8 int data[NUM] = {0};
    9 
   10 void Backup()
   11 {
   12     // father
   13     pid_t id = fork();
   14     if(id == 0)
   15     {
   16         // child
   17         int i = 0;
   18         printf("Backip: ");
   19         for(i = 0; i < NUM; i++)
   20         {
   21             printf("%d", data[i]);
   22         }
   23         printf("\n");
   24         sleep(10);
E> 25         exit(0); // 进程结束
   26     }
   27 }
   28 
   29 void ChangeData()
   30 {
   31     int i = 0;                                                         
   32     for(; i < NUM; i++)
   33     {
E> 34         data[i] = i + rand();
   35     }
   36     printf("origin data: ");
   37     for(i = 0; i < NUM; i++)
   38     {
   39         printf("%d ", data[i]);
   40     }
   41     printf("\n");
   42 }
   43 
   44 int main()
   45 {
E> 46     srand(time(NULL));
   47     while(1)
   48     {
   49         // 修改
E> 50         ChangeData();
   51         // 备份
   52         Backup();
   53         sleep(5);
   54     }
   55 }

2.进程终止

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

2.1退出场景

正常退出:代码跑完,运算结果正确;代码跑完,结果不正确。

异常退出:代码没跑完,出现异常(比如除零错误)(收到了信号,导致异常)。

退出信号举例:

2.2常见退出方法

从main返回;调用exit;_exit

2.2.1退出码

int main() { return 0; } ,0表示进程的退出码,表示进程执行情况。0表示成功,非0表示失败。退出码是给父进程的,如:bash或者自定义父进程。

退出码可以让父进程知道子进程将任务完成的结果如何。

示例:

关于$?:?是一个变量名,保存的是bash命令行上运行的最近一个退出进程的退出码

常见退出码:

2.2.2strerror

将错误码转换为字符串描述的函数。

部分结果:

2.2.3错误码和退出码区别

维度 错误码 退出码
粒度 细粒度(函数级) 粗粒度(进程级)
目的 诊断具体错误原因 表示整体成功/失败
使用者 程序员(调试/处理) 系统/父进程(流程控制)
时间点 运行时 程序结束时
持久性 临时,会被覆盖 最终,传递给父进程
标准化 POSIX定义部分 Shell惯例为主

核心区别 :错误码用于程序内部 的错误诊断和处理,而退出码用于进程间的成功/失败通信。一个程序内部可以使用多种错误码,但最终只能有一个退出码来总结整个程序的执行结果。

衡量一个进程运行结果是否"可信",其实可以用两个数字表示:exit code、signal number,当一个进程出现异常了,退出码没有意义:signal number != 0, exit code无意义。而这两个数字会出现在僵尸进程的pcb中。

2.2.4进程正常退出

1.main函数中return

在函数中调用return是指函数调用结束,在main中调用return是指进程结束

2.调用exit()

int status就是指退出码

结果:

可见,任意地方调用exit都表示进程结束。

3.调用_exit()

其实践结果与exit()相同,说明:虽然status是int,但是仅有低8位可以被⽗进程所用。_exit(-1)时,在终端执⾏$?发现返回值是255。

但是建议使用exit(),因为exit最后也会调用_exit,但在调用_exit之前,还做了其他工作:

1.执行用户通过 atexit或on_exit定义的清理函数。

2.关闭所有打开的流,所有的缓存数据均被写入

3.调用_exit

复制代码
int main()
{
 printf("hello");
 exit(0);
}
运⾏结果:
[root@localhost linux]# ./a.out
hello[root@localhost linux]#
int main()
{
 printf("hello");
 _exit(0);
}
运⾏结果:
[root@localhost linux]# ./a.out
[root@localhost linux]# 

3.进程等待

3.1进程等待必要性

由于子进程退出,父进程如果不管不顾,就可能造成'僵尸进程"的问题,进而造成内存泄漏。

并且,进程一旦变成僵尸状态,那就刀枪不入,"杀人不眨眼"的kil-9也无能为力,因为谁也没有办法杀死一个已经死去的进程。

最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是

不对,或者是否正常退出。

父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息

3.2进程等待的方法

3.2.1wait方法

复制代码
返回值:
 成功返回被等待进程pid,失败返回-1。
参数:
 输出型参数,获取⼦进程退出状态,不关⼼则可以设置成为NULL

解决僵尸问题:

复制代码
 1 #include <stdio.h>
 2 #include <unistd.h>
 3 #include <time.h>
 4 #include <stdlib.h>
 5 #include <string.h>
 6 #include <sys/wait.h>
 7 #include <sys/types.h>
 8 
 9 int main()
 10{
 11     printf("父进程:pid: %d, ppid: %d\n", getpid(), getppid());
 12 
 13     pid_t id = fork();
 14     if(id < 0)        
 15     {         
 16         perror("fork");
 17         exit(1);                      
 18     }                                  
 19     if(id == 0) 
 20     {                                  
 21         int cnt = 5;                  
 22         while(cnt)   
 23         {          
 24             printf("子进程:pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt); 
 25             sleep(1);
 26             cnt--;   
 27         }         
 28         printf("子进程退出\n");
 29         exit(10);              
 30     }                                                                    
 31 
 32     sleep(10);
 33               
 34     // 父进程
 35              
 36     pid_t rid = wait(NULL);
 37     if(rid > 0)
 38     {
 39         printf("等待子进程成功\n");
 40     }
 41 
 42     sleep(5);
 43 
 44     return 0;
 45 }

结论:

1.原则上,一般都是要保证父进程最后退出

2.父进程要通过wait等待子进程

3.如果子进程不退出,父进程就会阻塞在wait这里,等待子进程死亡

3.2.2waitpid方法

通过pid_t pid可以让父进程获取子进程退出信息。options是等待方式的获取:1.默认阻塞等待2.非阻塞

复制代码
返回值:
 当正常返回的时候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。

如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息。

如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。

如果不存在该子进程,则立即出错返回。

waitpid(-1,status,0) == wait(status)

测试代码:

复制代码
  1 #include <stdio.h>
  2 #include <unistd.h>
  3 #include <time.h>
  4 #include <stdlib.h>
  5 #include <string.h>
  6 #include <sys/wait.h>
  7 #include <sys/types.h>
  8 
  9 int main()
 10 {
 11     printf("父进程:pid: %d, ppid: %d\n", getpid(), getppid());
 12 
 13     pid_t id = fork();
 14     if(id < 0)
 15     {
 16         perror("fork");
 17         exit(1);
 18     }
 19     if(id == 0)
 20     {
 21         int cnt = 5;
 22         while(cnt)
 23         {
 24             printf("子进程:pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);
 25             sleep(1);
 26             cnt--;
 27         }
 28         printf("子进程退出\n");
 29         exit(1);
 30     }
 31                                                                                        
 32     // 父进程
 33     // pid_t rid = wait(NULL);
 34     int status = 0;
 35     pid_t rid = waitpid(id, &status, 0);
 36     if(rid > 0)
 37     {
 38         printf("等待子进程成功, status: %d\n", status);
 39     }
 40 
 41     return 0;
 42 }

运行结果发现status并没有获得退出码1:

3.2.3获取子进程status

wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。如果传NULL,表示不关心子进程的退出状态信息。

否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。

status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特位):

status当中有32为比特位,其中高16位我们不使用,又将低16位分为次低8位和低8位,其中次低8位保存退出码,第七位表示退出信号,其中一位表示core dump标志。

所以对代码做以下修改:

那么为什么退出码为1时,status为256?

因为退出码为1,也就是说明第八位为1,而其后有8个0跟着,2^8也就是256。

结论:退出码的取值范围:[0,255]

提取退出信号:

父进程获取子进程的退出信息 = 退出码 + 退出信号

3.2.4如何获取到子进程退出信息

1.子进程退出的退出信息,是维护在子进程的PCB中(包括将自己设置为僵尸)

2.父进程wait子进程,本质就是去读取子进程PCB内部的退出信息

总结:检查子进程z状态,获取子进程task_struct内部记录的子进程退出信息数据。

3.2.5waitpid中的options

0:阻塞等待。只要是内核数据结构,内部就会有维护队列,所以当等待时就是将PCB投递到要被等待对象的数据队列里,状态由R变为S

WNOHANG:宏,表示非阻塞等待

阻塞等待过程中,父进程会卡在指定位置,什么都不做,非阻塞等待过程中父子进程之间会出现非阻塞轮询过程,并且函数调用之后会立即返回。

非阻塞轮询代码:其中有用于测试的野指针

复制代码
  1 #include <stdio.h>
  2 #include <stdlib.h>
  3 #include <unistd.h>
  4 #include <time.h>
  5 #include <stdlib.h>
  6 #include <string.h>
  7 #include <sys/wait.h>
  8 #include <sys/types.h>
  9 int main()
 10 {
 11     printf("父进程: pid: %d, ppid: %d\n", getpid(), getppid());
 12     pid_t id = fork();
 13     if(id < 0)
 14     {
 15         perror("fork");
 16         exit(1);
 17     }
 18     if(id == 0)
 19     {
 20         int cnt = 5;
 21         while(cnt)
 22         {
 23             printf("子进程: pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);
 24             sleep(5);
 25             cnt--;
 26             int *p = NULL;
 27             *p = 100;
 28         }
 29         printf("子进程退出\n");
 30         exit(0);
 31     }                                                                                                                                                    
 32     
 33     while(1)
 34     {
 35         //父进程
 36         int status = 0;
 37         pid_t rid = waitpid(id, &status, WNOHANG);
 38         if(rid > 0)
 39         {
 40             printf("等待子进程成功,who: %d, status:%d, exit code: %d, exit sig: %d\n", rid, status, (status>>8)&0xFF, status & 0x7F);
 41             break;
 42         }
 43         else if(rid == 0)
 44         {
 45             sleep(1);
 46             printf("子进程还没有退出,父进程轮询\n");
 47         }
 48         else 
 49         {
 50             printf("等待子进程失败,who: %d, status:%d\n", rid, status);
 51             break;
 52         }
 53     }
 54     return 0;
 55 }

在父进程等待时可以做它自己的事情,代码:

复制代码
  1 #include <stdio.h>
  2 #include <stdlib.h>
  3 #include <unistd.h>
  4 #include <time.h>
  5 #include <stdlib.h>
  6 #include <string.h>
  7 #include <sys/wait.h>
  8 #include <sys/types.h>
  9 
 10 void PrintLog()
 11 {
 12     printf("我要打印日志!\n");
 13 }
 14                                                                                                                                                          
 15 void SyncMySQL()
 16 {
 17     printf("我要访问数据库!\n");
 18 }
 19 
 20 void Download()
 21 {
 22     printf("我要下载核心数据\n");
 23 }
 24 
 25 typedef void(*task_t)();
 26 
 27 task_t tasks[3] = {
 28     PrintLog,
 29     SyncMySQL,
 30     Download
 31 };
 32 
 33 int main()
 34 {
 35     printf("父进程: pid: %d, ppid: %d\n", getpid(), getppid());
 36     pid_t id = fork();
 37     if(id < 0)
 38     {
 39         perror("fork");                                                                                                                                  
 40         exit(1);
 41     }
 42     if(id == 0)
 43     {
 44         int cnt = 5;
 45         while(cnt)
 46         {
 47             printf("子进程: pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);
 48             sleep(5);
 49             cnt--;
 50             int *p = NULL;
 51             *p = 100;
 52         }
 53         printf("子进程退出\n");
 54         exit(0);
 55     }
 56     
 57     while(1)
 58     {
 59         //父进程
 60         int status = 0;
 61         pid_t rid = waitpid(id, &status, WNOHANG);
 62         if(rid > 0)
 63         {
 64             printf("等待子进程成功,who: %d, status:%d, exit code: %d, exit sig: %d\n", rid, status, (status>>8)&0xFF, status & 0x7F);
 65             break;
 66         }
 67         else if(rid == 0)
 68         {
 69             sleep(1);
 70             printf("子进程还没有退出,父进程轮询\n");
 71             for(int i = 0; i < 3; i++)
 72             {
 73                 tasks[i]();
 74             }
 75         }
 76         else 
 77         {
 78             printf("等待子进程失败,who: %d, status:%d\n", rid, status);
 79             break;
 80         }
 81     }
 82     return 0;
 83 }

在上述代码中,若想获得退出码,还需要我们进行位操作才可以,这样的做法显然太过麻烦,此后我们使用这两个宏,也可以完成这样的状态:

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

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

将上述代码中野指针取消掉:

4.编写一个一次等待多个子进程的代码

复制代码
  1 #include <iostream>
  2 #include <cstdlib>
  3 #include <cstdio>
  4 #include <unistd.h>
  5 #include <string>
  6 #include <vector>
  7 #include <sys/types.h>
  8 #include <sys/wait.h>
  9 
 10 const int gnum = 5;
 11 
 12 
 13 void Work()
 14 {
 15     int cnt = 5;
 16     while(cnt)
 17     {
 18         printf("%d work..., cnt: %d\n", getpid(), cnt--);
 19         sleep(1);
 20     }
 21 }
 22 
 23 int main()
 24 {
 25     std::vector<pid_t> subs;
 26     for(int idx = 0; idx < gnum; idx++)
 27     {
 28         pid_t id = fork();
 29         if(id < 0)
 30             exit(1);
 31         else if(id == 0)                                                                                                                                 
 32         {
 33             //child
 34             Work();
 35             exit(0);
 36         }
 37         else
 38         {
 39             subs.push_back(id);
 40         }
 41     }
 42 
 43     for(auto &sub : subs)
 44     {
 45         int status = 0;
 46         pid_t rid = waitpid(sub, &status, 0);
 47         if(rid > 0)
 48         {
 49             if(WIFEXITED(status))
 50             {
 51                 printf("child quit normal, exit code: %d\n", WEXITSTATUS(status));
 52             }
 53             else
 54             {
 55                 printf("%d child quit error!\n", sub);
 56             }
 57         }
 58     }
 59 
 60 
 61     return 0;
 62 }

本章完。

相关推荐
yuluo_YX3 小时前
Alias for Linux/Mac
linux·elasticsearch·macos
冉佳驹4 小时前
Linux ——— 磁盘存储原理与文件系统工作机制
linux·磁盘·硬链接·inode·软链接·磁盘线性化·目录的本质
BUG_MeDe4 小时前
LINUX MTU/MSS(1500 1460等)的一些理解
linux·运维·服务器
风流倜傥唐伯虎4 小时前
Windows 版 Docker 的 Linux 环境(docker-desktop)与 builder-jammy-base:latest 镜像核心区别
linux·docker·容器
曹牧4 小时前
Nginx:正向代理与反向代理
运维·nginx
Ha_To4 小时前
2026.1.30 搭建docker仓库
运维·docker·容器
lpfasd1234 小时前
Docker Desktop 在国内使用的囧境:镜像拉取失败、加速器失效与破局之道
运维·docker·容器
江湖有缘4 小时前
Docker部署SurveyKing调查问卷系统和考试系统
运维·docker·容器
小宇的天下4 小时前
Cadence allegro---assign net
服务器·php·apache