进程控制详解

🎬 个人主页Vect个人主页

🎬 GitHubVect的代码仓库
🔥 个人专栏 : 《数据结构与算法》《C++学习之旅》《Linux

⛺️Per aspera ad astra.



文章目录

1. 进程创建

fork函数

根据文档描述:

fork函数的作用是创建一个子进程

返回值:

  • >0 把子进程的pid返回给父进程
  • ==0 说明创建的是子进程
  • -1 说明进程创建失败

进程调用fork,当控制转移到内核中fork代码后,内核做:

  1. 分配新的内存块和内核数据结构给子进程
  2. 将父进程部分数据内容拷贝给子进程
  3. 添加子进程到系统进程列表中
  4. for返回,调度器开始调度

看如下代码:

cpp 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

int main(){
  printf("before: pid: %d\n",getpid());

  pid_t id = fork();
  if(id < 0){
    perror("fork");
    return 1;
  }else if(id == 0){
    printf("after: child, pid:%d, return val: %d\n",getpid(),id);
  }else{
    printf("after: fatehr, pid:%d, return val: %d\n",getpid(),id);
  }

  return 0;
}

输出结果:

bash 复制代码
[vect@VM-0-11-centos process_control]$ make
gcc -o show_fork show_fork.c
[vect@VM-0-11-centos process_control]$ ./show_fork
before: pid: 9928
after: fatehr, pid:9928, return val: 9929
after: child, pid:9929, return val: 0

frok之前,父进程执行打印,此时没有创建子进程

fork之后,父子进程分别执行各自的打印操作

也就是说,fork之前父进程独立执行,fork之后父子进程执行流分别执行

为什么子进程返回0,父进程返回子进程的pid?

一个父进程可以有多个子进程,而一个子进程只能有一个父进程。对于父进程,子进程需要被标记,父进程可以根据子进程的pid返回值更好的管理子进程;而对于子进程来说,父进程无需被标记

为什么fork有两个返回值?

并不是fork返回两次,而是fork调用一次会创建一个子进程,父子进程会从fork返回处继续执行,各自拿到一个返回值

写时拷贝

fork创建子进程时,内核不会立刻复制父进程的物理内存,而是做两件事:

  • 共享物理内存: 父子进程共享同一块物理内存空间(包括代码和数据)
  • 标记页表项为 "只读":将父子进程页表中指向该共享物理内存的页表项权限均设置为 "只读";同时保证父子进程的虚拟地址通过各自的页表项,映射到同一块物理内存页

当父进程或子进程试图修改这块内存 时,内核触发缺页异常 , 进行缺页中断,随即开始检测,判定有进程要发生写时拷贝------为要修改的的内存创建独立的物理内存副本,然后让修改方指向这个新副本,另一方仍指向原内存

详细过程如下:

  1. fork 调用,创建子进程

    • 父进程调用fork,内核创建子进程的PCB,复制父进程的页表
    • 内核将父子进程页表项的代码和数据标记为"只读"
    • 父子进程的页表映射到同一块物理内存
    • 返回两个进程的返回值,此时父子进程完全共享内存
  2. 无修改操作->持续共享内存

    • 若父子进程都只读取内存,永远不触发写实拷贝
  3. 有操作修改->触发写时拷贝

    当父子进程试图写入某块内存时:

    • CPU触发缺页异常
    • 内核捕获异常,检查是写时拷贝导致的异常
    • 内核为被修改的这个内存分配新的物理内存
    • 内核将原内存的内容拷贝到新的内存
    • 修改触发写操作的页表项,指向新的物理内存
    • 取消新内存的只读标记,原内存保持只读
    • 异常处理结束,CPU重新执行写操作,此时写入新副本

fork的用法

  • 一个父进程希望复制自己,使父子进程执行不同的代码段
  • 一个进程要执行一个不同的程序

fork失败的原因

  • 系统进程太多
  • 实际用的进程数超过了限制

2. 进程终止

进程终止的本质是释放内核数据结构和对应代码与数据

有三个场景:

  • 代码运行完毕,结果正确->返回0
  • 代码运行完毕,结果错误->返回非0
  • 代码异常终止->本质是OS用信号提前终止了进程

进程退出方法

正常终止的进程可以通过echo $?查看最近一次进程的退出码

bash 复制代码
./look_exit
hh
[vect@VM-0-11-centos process_control]$ echo $?
0
[vect@VM-0-11-centos process_control]$ lsls
-bash: lsls: command not found
[vect@VM-0-11-centos process_control]$ echo $?
127
  • main函数返回
  • 调用exit
  • _exit

异常退出:

ctrl+c,信号终止

退出码

退出码可以告诉我们最后一次执行的进程的状态。在命令结束后,可以根据退出码知道进程以什么方式终止

Linux Shell中的退出码有

bash 复制代码
0:Success
1:Operation not permitted
2:No such file or directory
3:No such process
4:Interrupted system call
5:Input/output error
6:No such device or address
7:Argument list too long
8:Exec format error
9:Bad file descriptor
10:No child processes
11:Resource temporarily unavailable
12:Cannot allocate memory
13:Permission denied
14:Bad address
15:Block device required
16:Device or resource busy
17:File exists
18:Invalid cross-device link
19:No such device
20:Not a directory
21:Is a directory
22:Invalid argument
23:Too many open files in system
24:Too many open files
25:Inappropriate ioctl for device
26:Text file busy
27:File too large
28:No space left on device
29:Illegal seek
30:Read-only file system
31:Too many links
32:Broken pipe
33:Numerical argument out of domain
34:Numerical result out of range
35:Resource deadlock avoided
36:File name too long
37:No locks available
38:Function not implemented
39:Directory not empty
40:Too many levels of symbolic links
41:Unknown error 41
42:No message of desired type
43:Identifier removed
44:Channel number out of range
45:Level 2 not synchronized
46:Level 3 halted
47:Level 3 reset
48:Link number out of range
49:Protocol driver not attached
50:No CSI structure available
51:Level 2 halted
52:Invalid exchange
53:Invalid request descriptor
54:Exchange full
55:No anode
56:Invalid request code
57:Invalid slot
58:Unknown error 58
59:Bad font file format
60:Device not a stream
61:No data available
62:Timer expired
63:Out of streams resources
64:Machine is not on the network
65:Package not installed
66:Object is remote
67:Link has been severed
68:Advertise error
69:Srmount error
70:Communication error on send
71:Protocol error
72:Multihop attempted
73:RFS specific error
74:Bad message
75:Value too large for defined data type
76:Name not unique on network
77:File descriptor in bad state
78:Remote address changed
79:Can not access a needed shared library
80:Accessing a corrupted shared library
81:.lib section in a.out corrupted
82:Attempting to link in too many shared libraries
83:Cannot exec a shared library directly
84:Invalid or incomplete multibyte or wide character
85:Interrupted system call should be restarted
86:Streams pipe error
87:Too many users
88:Socket operation on non-socket
89:Destination address required
90:Message too long
91:Protocol wrong type for socket
92:Protocol not available
93:Protocol not supported
94:Socket type not supported
95:Operation not supported
96:Protocol family not supported
97:Address family not supported by protocol
98:Address already in use
99:Cannot assign requested address
100:Network is down
101:Network is unreachable
102:Network dropped connection on reset
103:Software caused connection abort
104:Connection reset by peer
105:No buffer space available
106:Transport endpoint is already connected
107:Transport endpoint is not connected
108:Cannot send after transport endpoint shutdown
109:Too many references: cannot splice
110:Connection timed out
111:Connection refused
112:Host is down
113:No route to host
114:Operation already in progress
115:Operation now in progress
116:Stale file handle
117:Structure needs cleaning
118:Not a XENIX named type file
119:No XENIX semaphores available
120:Is a named type file
121:Remote I/O error
122:Disk quota exceeded
123:No medium found
124:Wrong medium type
125:Operation canceled
126:Required key not available
127:Key has expired
128:Key has been revoked
129:Key was rejected by service
130:Owner died
131:State not recoverable
132:Operation not possible due to RF-kill
133:Memory page has hardware error

我们用这段代码可以获得:

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
int main(){
  for(int i = 0; i < 200; i++){
    printf("%d:%s\n",i,strerror(i));
  }
  return 0;
}

进程正常退出

main函数的return

main函数中使用return退出进程是我们最常用的方法,但是这里需要注意:

普通函数的return只代表这个函数结束,并不代表整个进程结束

_exit函数和exit函数

_exitexit可以在代码的任何地方退出

我们先来看区别:

分析之前我们需要搞清楚标准IO缓冲区

缓冲区的本质: 标准IO库为了减少系统调用(写道中断/文件是昂贵的内核操作),在用户语言层面开辟的一块内存区域,暂时存放输出数据,满足条件时才把数据刷到内核(最终输出到终端/文件)

分析:

exit标准C库函数 ->用户语言层面,_exit系通调用 ->内核层面,二者核心差异是是否执行用户语言层面的清理操作,尤其是缓冲区刷新

很显然,exit会刷新完缓冲区再退出,而_exit直接退出

所以,我们现在所说的缓冲区是用户语言级别的!!!不在OS内部!!!

所以,对于用户级别的exit是封装了内核级别的_exit


3.进程等待

进程等待的必要性

  • 子进程退出,变成僵尸态,等待父进程处理,若不进行处理,可能会造成内存泄漏
  • 进程一旦变成僵尸态,kill -9也无能为力,谁也没办法杀死一个已经死去的进程
  • 父进程拍给子进程的任务的完成情况,需要反馈

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

获取子进程的status

子进程的status就是退出码,仅仅从终止后,向父进程返回的一个整数值,它的作用是:

  • 子进程是正常退出 or异常终止
  • 正常退出,结果是否正确,异常终止,终止原因是什么

Linux下status的二进制结构

status是一个16位的整数,位段划分如下图:

  • 正常退出:高8位有效,低8位为0
  • 信号终止:低8位有效,高8位无效

我们可以通过位运算的方式得到退出码:

  • exit_code = (status >> 8) & 0xff;

    退出码在高8存储,且退出码的范围是[0,255],需要将这8位数右移到低8位,然后只保留低8位,则按位与0xff->

    1111 1111

  • exit_signal = status & 0x7f;

    信号存在低7位,保留低7位即可,按位与0x7f->111 1111

对此,系统提供了两个宏来获取退出码和退出信号:

cpp 复制代码
#define WIFEXITED(status)  (((status) & 0x7F) == 0)

#define WEXITSTATUS(status) (((status) >> 8) & 0xFF)

需要注意的是:一个进程非正常退出,说明是被信号终止,那么该进程的退出码就没有意义了

进程等待方法

wait

cpp 复制代码
#include <sys/types.h>
#include <sys/wait.h>

pid_t wait(int* status);

作用:

​ 等待任意子进程

返回值:

​ 成功返回:被等待进程的pid 失败返回:-1

参数:

​ 输出型参数,获取子进程退出状态,不关心可以设成NULL

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>

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

  // 父进程
  int status = 0;
  // pid_t wait(int* status);
  pid_t ret = wait(&status);  // 等待子进程 获取子进程返回码
  if(ret > 0){
    // 等待成功
    printf("successful wait\n");
    if(WIFEXITED(status)){ // 判断是否正常退出
      printf("exit code: %d\n",WEXITSTATUS(status));  // 提取退出码
    }
  }
  sleep(3);

  return 0;
}

输出结果:

bash 复制代码
[vect@VM-0-11-centos process_control]$ gcc -o wait wait.c
[vect@VM-0-11-centos process_control]$ ./wait
I am child, pid: 4197, ppid: 4196
I am child, pid: 4197, ppid: 4196
I am child, pid: 4197, ppid: 4196
I am child, pid: 4197, ppid: 4196
I am child, pid: 4197, ppid: 4196
I am child, pid: 4197, ppid: 4196
successful wait
exit code: 0

waitpid

cpp 复制代码
pid_t waitpid(pid_t pid, int* status, int options)

作用:

​ 等待指定子进程或任意子进程

返回值:

  • 正常返回时返回子进程pid
  • 若设置了选项WNOHANG,而调用中waitpid中发现没有已经退出的子进程可以收集,返回0
  • 调用出错,返回-1,这时errno会被设置成相应的值指示错误

参数:

  • PID:

    • pid=-1,等待任意一个子进程,与wait等效
    • pid>0,等待这个pid的子进程
  • status:

    输出型参数,获取子进程退出状态,不关心可以设成NULL

  • options:

    当设置成WNOHANG时,若等待的子进程没有结束,直接返回0,不等待;若正常结束,返回该子进程的pid

例如,创建子进程后,父进程可以一直等待子进程,waitpid的第三个参数设置为0,指导子进程退出后读取子进程的退出信息

cpp 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>

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

  // 父进程
  int status = 0;
  pid_t ret = waitpid(id, &status, 0);
  if(ret >= 0){ // 等待成功
    printf("wait successfully\n");
    if(WIFEXITED(status)){    // 正常返回
      printf("exit code: %d\n",WEXITSTATUS(status));
    }else{  // 异常返回 被信号杀死
      printf("killed by signal, %d\n",status & 0x7F);
    }
  }
  sleep(3);

  return 0;

}

进程被杀信号杀死,也可以成功等待子进程

注意:被信号杀死的进程,其退出码没有意义

阻塞等待与非阻塞等待

阻塞等待方式

当子进程未退出时,父进程一直在等待子进程,等待期间,父进程不能做任何事情,这种等待方式称为阻塞等待

cpp 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdlib.h>

int main(){
  pid_t pid = fork();
  if(pid < 0){
    printf("fork err");
    exit(1);
  }
  else if(pid == 0){  // 子进程
    printf("I am child, pid: %d\n",getpid());
    sleep(5);
    exit(257);
  }else{
    int status = 0;
    pid_t ret = waitpid(-1,&status,0);  // 阻塞等待,等待5s
    printf("waiting...\n");
    if(WIFEXITED(status) && ret == pid){
      printf("wait child 5s successfully, child return code: %d\n",WEXITSTATUS(status)); 
    }else{
      printf("wait child failed\n");
      exit(1);
    }
  }
  return 1;
}

输出结果:

bash 复制代码
[vect@VM-0-11-centos process_control]$ gcc -o block_wait block_wait.c
[vect@VM-0-11-centos process_control]$ ./block_wait 
I am child, pid: 17498
waiting...
wait child 5s successfully, child return code: 1

非阻塞等待的轮询检测方式

而实际上,我们完全没有必要让父进程干等着,父进程可以在等待子进程的同时做其他事情------非阻塞等待

waitpid的第三个参数传WNOGANG,若等待的子进程没有结束,那么waitpid函数直接返回0,不等待;若子进程正常结束,返回该子进程的pid

cpp 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdlib.h>

int main(){
  pid_t id = fork();
  if(id < 0){
    perror("fork err\n");
    exit(1);
  }else if(id == 0){  // child
    int count = 5;
    while(count--){
      printf("child runing... pid: %d\n",getpid());
      sleep(3);
    }
    exit(0);
  }else{  // father
    while(1){
      int status = 0;
      pid_t ret = waitpid(id,&status,WNOHANG);
      if(ret > 0){
        printf("wait child successfully\n");
        printf("child exit code:%d\n",WEXITSTATUS(status));
        break;
      }else if(ret == 0){
        printf("father does other things...\n");
        sleep(1);
      }else{
        printf("wait err...\n");
        break;
      }
    }
  
  }

  return 0;
}

父进程每隔一段时间查看子进程是否退出;若为退出,父进程忙自己的事情,每隔一段时间来查看一次,直到子进程退出读取其退出信息


4. 进程程序替换

替换原理

fork创建子进程后,子进程执行的是和父进程相同的程序(可能执行不同的代码分支),想要让子进程执行另一个程序,需要调用函数完成->exex系列函数

当进程调用exec系列函数时,该进程的空间、代码和数据完全被新程序替换,并从新程序的启动历程开始执行

当进程替换时,有没有创建新的进程?

进程程序替换之后,该进程对应的PCB、进程地址空间及页表等都没有发生改变,仅仅改变进程在物理内存中的代码和数据,所以没有创建新的进程,并且进程程序替换前后该进程的pid没有改变

bash 复制代码
ls
block_wait.c  Makefile         other              show_fork.c
exec.c        myexec           other.c            wait.c
exit.c        no_block_wait.c  print_exit_code.c  waitpid.c
[vect@VM-0-11-centos process_control]$ make
gcc -o myexec exec.c
[vect@VM-0-11-centos process_control]$ ./myexec 
我是 exec, pid: 22591
我是other, pid: 22591[vect@VM-0-11-centos process_control]$ 

pid相同,所以没有创建新的进程

子进程进行程序替换后,会影响父进程的代码和数据吗?

不会,子进程刚被创建,和父进程共享代码和数据,但当子进程进程程序替换时,就意味着子进程需要对其数据和代码进行写入操作,这时候发生写时拷贝,此后父子进程代码和数据分离

替换函数

有六种以exec 开头的函数,统称为exec函数

cpp 复制代码
#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[]);

函数解释

  • 调用成功,不用返回
  • 调用出错,返回-1

所以,exec只要返回了,就是调用失败

怎么理解这些函数

  • l(list):参数采用列表
  • v(vector):参数用数组
  • p(path):有p则自动搜索环境变量PATH,不用带详细路径
  • e(env):表示自己维护环境变量

函数使用

cpp 复制代码
execl("/bin/ls","ls","-al","--color",NULL);

第一个参数path是带路径的指令,想执行谁,第二个参数是可变参数列表,想要怎么执行

cpp 复制代码
char* argv[] = {"ls","-l",NULL};
execv("/bin/ls",argv);

第一个参数path是带路径的指令,想执行谁,第二个参数是char*的数组,想要怎么执行

这个和命令行参数的第二个参数一样的形式,都是一张表

cpp 复制代码
execlp("ls","ls","--color",NULL);   

第一个参数是不带路径的命令,想执行谁,第二个参数是可变参数列表,想要怎么执行。这里同时出现两个参数ls,表示的语义不一样,参数位置也不一样

cpp 复制代码
char* argv[] = {"ls","-l",NULL};
execvp("ls",argv);

这里不做解释了

cpp 复制代码
char* argv[] = {"ls","-l",NULL};
execvpe("ls",argv,NULL);

第一个参数是不带路径的命令,想执行谁,第二个参数是char*的数组,想要怎么执行,第三个参数是新的环境变量

对于环境变量:

  • 不传参:会默认继承父进程的环境变量表
  • 手动传参:会用全新的环境变量
  • 单纯新增环境变量:使用putenv函数,谁调用了putenv,就把putenv里的环境变量导入到调用者的环境变量表

一般来说,我们不需要手动传环境变量,系统默认的就够了,可以得到一个结论:程序替换是不影响命令行参数和环境变量的

cpp 复制代码
int execve(const char *path, char *const argv[], char *const envp[]);

第一个参数是要执行程序的路径,想执行谁,第二个参数是一个char*数组,想要怎么执行,第三个参数是新的环境变量

事实上,只有execve是真正的系统调用,其他五个函数都是调用execve封装的函数。所以execveman手册的第2节,而其它五个函数在man手册的第3节

相关推荐
姚青&2 小时前
Linux 命令介绍以及帮助命令介绍
linux·运维·服务器
wdfk_prog2 小时前
[Linux]学习笔记系列 -- [fs]fs-writeback
linux·笔记·学习
遇见火星2 小时前
详解 Linux 中的 /etc/fstab 文件
linux·运维·服务器
menggb072 小时前
在Linux系统上安装和使用Prometheus+Grafana
linux·运维·prometheus
wregjru3 小时前
【操作系统】linux常用指令
linux·运维·服务器
徐安安ye4 小时前
Flutter 车载系统开发:打造符合 Automotive Grade Linux 标准的 HMI 应用
linux·flutter·车载系统
lifewange5 小时前
Linux 服务管理故障排查小手册
linux·运维·服务器
LUCIFER5 小时前
[驱动之路(九)——UART(串口)子系统]学习总结,万字长篇,一文彻底搞懂UART(串口)子系统(含串口数据收发流程解析)
linux·驱动开发
忙里偷闲学python5 小时前
ceph介绍和安装
linux·ceph·kubernetes