【Linux】系统部分——进程控制
11.进程控制
文章目录
- 11.进程控制
- 一、进程创建
- 二、进程终止
- 退出码
- 进程终止的方式
- 三、进程等待
- 进程等待的方式
- 获取⼦进程status
- 小程序
- 阻塞与非阻塞等待
- 四、进程程序替换
- 替换原理
- 进程程序替换的接口——exec替换函数
- 五、总结
一、进程创建
之前学习了fork()函数创建子进程,进程调⽤fork,当控制转移到内核中的fork代码后,内核做:分配新的内存块和内核数据结构给⼦进程;将⽗进程部分数据结构内容拷⻉⾄⼦进程;添加⼦进程到系统进程列表当中;fork返回,开始调度器调度 。其中将⽗进程部分数据结构内容拷⻉⾄⼦进程的方法是写时拷贝。下面进行说明:
通常,⽗⼦代码共享,⽗⼦再不写⼊时,数据也是共享的,当任意⼀⽅试图写⼊,便以写时拷⻉的⽅式各⾃⼀份副本。 因为有写时拷⻉技术的存在,所以⽗⼦进程得以彻底分离!完成了进程独⽴性的技术保证! 写时拷⻉,是⼀种延时申请技术,可以提⾼整机内存的使⽤率
那么在修改内容之前与修改内容之后,具体做了什么?
在fork时将父进程的所有物理地址权限都改为只读,之后子进程再拷贝父进程的内容。这样当子进程或父进程对数据进行写入操作的时候就会触发系统错误。之后系统会触发缺页中断,之后系统检测,是真的发生错误还是发生写时拷贝。若判定为写时拷贝,系统会进行申请内存—>发生拷贝(将原数据段内容拷贝到新申请的内存中)—>修改页表—>恢复执行,同时将数据区的权限设置为读写(父子进程都会改)。
二、进程终止
退出码
main函数的返回值其实是返回给父进程和系统的。
[lisihan@hcss-ecs-b735 lession15]$ cat code1.cpp
#include<iostream>int main()
{std::cout << "hello linux" << std::endl;return 123;
}[lisihan@hcss-ecs-b735 lession15]$ ./code1
hello linux
[lisihan@hcss-ecs-b735 lession15]$ echo $?
123
[lisihan@hcss-ecs-b735 lession15]$ echo $?
0
-
通过
$?
我们可以查到上一次程序执行的退出码,这里返回的就是123,并用echo显示出来,这个退出码用于表明错误原因 -
同样echo也是一个可执行程序,所以第二次
echo $?
打印的值实际上是上一个echo $?
的退出码,即为0 -
通常约定:0—成功 非0—失败
在C语言中提供了一批错误码errno
以及查看错误码对应错字符串的函数接口strerrno()
[lisihan@hcss-ecs-b735 lession15]$ cat code2.c
#include <stdio.h>
#include <string.h>
#include <errno.h>
int main()
{printf("before: errno: %d, strerrno: %s\n", errno, strerror(errno));FILE* fp = fopen("./text.c", "r");if(fp == NULL){printf("before: errno: %d, strerrno: %s\n", errno, strerror(errno));return errno;}return 0;
}
[lisihan@hcss-ecs-b735 lession15]$ ./code2
before: errno: 0, strerrno: Success
before: errno: 2, strerrno: No such file or directory
进程终止的方式
-
_exit()函数
这个函数是一个系统调用函数,参数是进程要传递的退出码
-
exit()函数
这个函数是C语言自带的函数,与_exit()的用法是一样的
-
return退出
return是⼀种更常⻅的退出进程⽅法。执⾏return n等同于执⾏exit(n),因为调⽤main的运⾏时函数会将main的返回值当做 exit的参数。
_exit()与exit()的区别:
exit最后也会调⽤_exit, 但在调⽤_exit之前,还做了其他⼯作 :
- 执⾏⽤⼾通过 atexit或on_exit定义的清理函数
- 关闭所有打开的流,所有的缓存数据均被写⼊
- 调⽤_exit
从这个过程可知:使用exit()退出进程会将缓存区的内容刷新出来,之后再关闭,而_exit()直接关闭进程,没有上述步骤。
三、进程等待
进程等待的方式
之前演示过当子进程运行结束而父进程没有回收子进程的话,子进程会一直处于僵尸状态,进⽽造成内存泄漏。并且⽗进程派给⼦进程的任务完成的如何,我们需要知道。如,⼦进程运⾏完成,结果对还是不对,或者是否正常退出。因此⽗进程需要通过进程等待的⽅式,回收⼦进程资源,获取⼦进程退出信息。
pid_t wait(int status);*
返回值:成功返回被等待进程pid,失败返回-1。
参数:输出型参数,获取⼦进程退出状态,不关⼼则可以设置成为NULL
*pid_ t waitpid(pid_t pid, int status, int options);
-
返回值:
当正常返回的时候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,⼦进程存在且正常运⾏,则进程可能阻塞。
- 如果不存在该⼦进程,则⽴即出错返回。
获取⼦进程status
- wait和waitpid,都有⼀个status参数,该参数是⼀个输出型参数,由操作系统填充。
- 如果传递NULL,表⽰不关⼼⼦进程的退出状态信息。
- 否则,操作系统会根据该参数,将⼦进程的退出信息反馈给⽗进程。
- status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16⽐特位):
解释:这张图表示status获取子进程状态之后数据的分布情况,可以看到只用到了低16位的bit位,但是分了两种不同的情况:
- 正常终止:8~15bit位储存进程退出信息也就是子进程返回的退出码
- 当子进程被信号所杀:0~7bit的位置储存终止信号的信息。
举例:
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>int main()
{pid_t id = fork();if(id < 0){printf("errno : %d, errstring: %s\n", errno, strerror(errno));return errno;}else if(id == 0){int cnt = 5;while(cnt){printf("子进程运行中, pid: %d\n", getpid());cnt--;sleep(1);}exit(123);}else{sleep(3);int status = 0;pid_t rid = waitpid(id, &status, 0); // == waitif(rid > 0){printf("wait sub process success, rid: %d, status code: %d, singal: %d\n", rid, status >> 8 & 0xFF, status & 0x0F );//这里我们可以用这种位运算的方法,也可以使用系统提供的宏WEXITSTATUS(status),这些在前面有//if(WIFEXITED(status))//{// printf("子进程正常推出!\n");//}//else//{// //.....//}}elseperror("waitpid");while(1){printf("我是父进程: pid:%d\n", getpid());sleep(1);}}return 0;
}
运行结果:
#运行窗口
[lisihan@hcss-ecs-b735 lession15]$ ./code3
子进程运行中, pid: 16597
子进程运行中, pid: 16597
子进程运行中, pid: 16597
子进程运行中, pid: 16597
子进程运行中, pid: 16597
wait sub process success, rid: 16597, status code: 123, singal: 0
我是父进程: pid:16596
我是父进程: pid:16596
我是父进程: pid:16596
我是父进程: pid:16596
我是父进程: pid:16596#监视窗口PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND #进程未启动
14111 16594 16593 14111 pts/2 16593 S+ 1000 0:00 grep --color=auto code3PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND #进程启动,创建父子进程
12990 16596 16596 12990 pts/1 16596 S+ 1000 0:00 ./code3
16596 16597 16596 12990 pts/1 16596 S+ 1000 0:00 ./code3
14111 16601 16600 14111 pts/2 16600 S+ 1000 0:00 grep --color=auto code3PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
12990 16596 16596 12990 pts/1 16596 S+ 1000 0:00 ./code3
16596 16597 16596 12990 pts/1 16596 S+ 1000 0:00 ./code3
14111 16606 16605 14111 pts/2 16605 S+ 1000 0:00 grep --color=auto code3PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
12990 16596 16596 12990 pts/1 16596 S+ 1000 0:00 ./code3
16596 16597 16596 12990 pts/1 16596 S+ 1000 0:00 ./code3
14111 16611 16610 14111 pts/2 16610 S+ 1000 0:00 grep --color=auto code3PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
12990 16596 16596 12990 pts/1 16596 S+ 1000 0:00 ./code3
16596 16597 16596 12990 pts/1 16596 S+ 1000 0:00 ./code3
14111 16616 16615 14111 pts/2 16615 S+ 1000 0:00 grep --color=auto code3PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
12990 16596 16596 12990 pts/1 16596 S+ 1000 0:00 ./code3
16596 16597 16596 12990 pts/1 16596 S+ 1000 0:00 ./code3
14111 16621 16620 14111 pts/2 16620 S+ 1000 0:00 grep --color=auto code3PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND #子进程运行结束,父进程回收子进程
12990 16596 16596 12990 pts/1 16596 S+ 1000 0:00 ./code3
14111 16626 16625 14111 pts/2 16625 S+ 1000 0:00 grep --color=auto code3PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
12990 16596 16596 12990 pts/1 16596 S+ 1000 0:00 ./code3
14111 16641 16640 14111 pts/2 16640 S+ 1000 0:00 grep --color=auto code3PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND #进程全部结束14111 16641 16640 14111 pts/2 16640 S+ 1000 0:00 grep --color=auto code3
此时我们可以谈谈关于进程退出:
- 子进程代码跑完之后,结果是否正确是用退出码判定的
- 子进程出现异常,比如说野指针、数据溢出等情况,OS会使用信号直接终止这个进程(有关信号的问题后面再谈),进程退出信息中,会记录自己的退出信号
- 进程的退出码和退出信号都会在进程的内核数据结构task_struct中维护
小程序
学习了上面的内容,我们可以写一个小程序,用于定时备份一个vector中的内容:
#include <iostream>
#include <cstdio>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <vector>
#include <string>std::vector<int> data;int save()
{pid_t id = fork();if(id == 0)//子进程进行拷贝操作{std::string name = "./backup/"; //拷贝文件放在backup文件夹中name += std::to_string(time(nullptr)); //文件名称为拷贝的时间#############################################################name += ".backup"; //文件名后缀FILE* fp = fopen(name.c_str(), "w"); //文件创建+打开if(fp == nullptr) return 1;std::string data_str;for(auto d :data) //把data中的数据转化为字符串储存在data_str中{data_str += std::to_string(d);data_str += " ";}fputs(data_str.c_str(), fp); //文件写入fclose(fp); //文件关闭exit(0);}else //父进程阻塞等待子进程拷贝完成{int status;pid_t pid = waitpid(id, &status, 0);if(pid > 0) //等待成功{printf("wait child process success!, exit code: %d\n", WEXITSTATUS(status));}else //等待失败{printf("wait child process...\n");}}return 0;
}int main()
{int cnt = 0;while(true){data.push_back(cnt++); //data数据写入sleep(1); if(cnt % 10 == 0)save(); //拷贝操作}return 0;
}
程序运行:
#运行窗口
[lisihan@hcss-ecs-b735 lession15]$ g++ -o code4 code4.cpp -std=c++11
[lisihan@hcss-ecs-b735 lession15]$ ./code4
wait child process success!, exit code: 0
wait child process success!, exit code: 0
wait child process success!, exit code: 0
wait child process success!, exit code: 0
wait child process success!, exit code: 0
wait child process success!, exit code: 0
wait child process success!, exit code: 0
wait child process success!, exit code: 0
wait child process success!, exit code: 0#监视窗口
[lisihan@hcss-ecs-b735 backup]$ ls
1748787298.backup 1748787308.backup 1748787354.backup 1748787364.backup 1748787374.backup
[lisihan@hcss-ecs-b735 backup]$ cat 1748787374.backup
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 [lisihan@hcss-ecs-b735 backup]$ ls
1748787298.backup 1748787364.backup 1748787394.backup 1748787424.backup
1748787308.backup 1748787374.backup 1748787404.backup 1748787434.backup
1748787354.backup 1748787384.backup 1748787414.backup
程序运行正常
阻塞与非阻塞等待
阻塞等待:如果子进程没有退出或终止,父进程会一直等待直到子进程退出,父进程才会执行之后的代码
非阻塞等待:即使子进程没有退出,父进程只会在waitpid函数中判断一次,不会一直在函数中等待子进程退出,在等待子进程退出的这段时间,父进程可以去做其他事情。
选择阻塞还是非阻塞等待由waitpid函数中的第三个形参决定的
options:
默认为0,表⽰阻塞等待
WNOHANG(理解为wait no hang): 若pid指定的⼦进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该⼦进程的ID。
具体就不演示了
四、进程程序替换
之前我们使用函数fork()
创建子进程, fork()
之后,⽗⼦各⾃执⾏⽗进程代码的⼀部分如果⼦进程就想执⾏⼀个全新的程序呢?进程的程序替换来完成这个功能。程序替换是通过特定的接⼝,加载磁盘上的⼀个全新的程序(代码和数据),加载到调⽤进程的地址空间中!
以execl()
函数为例,这个函数用于进程程序替换,后面会详细介绍,我们执行下面的代码:
#include <iostream>
#include <unistd.h>int main()
{execl("/bin/ls", "-a", "-l", nullptr); return 0;
}
运行结果:
[lisihan@hcss-ecs-b735 lession15]$ ./code5
total 108
drwxrwxr-x 2 lisihan lisihan 4096 Jun 1 22:17 backup
-rwxrwxr-x 1 lisihan lisihan 9024 May 31 18:31 code1
-rw-rw-r-- 1 lisihan lisihan 94 May 31 18:29 code1.cpp
-rwxrwxr-x 1 lisihan lisihan 8528 May 31 19:05 code2
-rw-rw-r-- 1 lisihan lisihan 313 May 31 19:04 code2.c
-rwxrwxr-x 1 lisihan lisihan 8784 Jun 1 20:40 code3
-rw-rw-r-- 1 lisihan lisihan 1053 Jun 1 21:37 code3.c
-rwxrwxr-x 1 lisihan lisihan 30040 Jun 1 22:15 code4
-rw-rw-r-- 1 lisihan lisihan 1002 Jun 1 22:17 code4.cpp
-rwxrwxr-x 1 lisihan lisihan 8760 Jun 2 20:48 code5
-rw-rw-r-- 1 lisihan lisihan 116 Jun 2 20:46 code5.cpp
-rw-rw-r-- 1 lisihan lisihan 176 Jun 2 20:47 makefile
[lisihan@hcss-ecs-b735 lession15]$ ls -a -l
total 116
drwxrwxr-x 3 lisihan lisihan 4096 Jun 2 20:48 .
drwx------ 22 lisihan lisihan 4096 May 31 18:27 ..
drwxrwxr-x 2 lisihan lisihan 4096 Jun 1 22:17 backup
-rwxrwxr-x 1 lisihan lisihan 9024 May 31 18:31 code1
-rw-rw-r-- 1 lisihan lisihan 94 May 31 18:29 code1.cpp
-rwxrwxr-x 1 lisihan lisihan 8528 May 31 19:05 code2
-rw-rw-r-- 1 lisihan lisihan 313 May 31 19:04 code2.c
-rwxrwxr-x 1 lisihan lisihan 8784 Jun 1 20:40 code3
-rw-rw-r-- 1 lisihan lisihan 1053 Jun 1 21:37 code3.c
-rwxrwxr-x 1 lisihan lisihan 30040 Jun 1 22:15 code4
-rw-rw-r-- 1 lisihan lisihan 1002 Jun 1 22:17 code4.cpp
-rwxrwxr-x 1 lisihan lisihan 8760 Jun 2 20:48 code5
-rw-rw-r-- 1 lisihan lisihan 116 Jun 2 20:46 code5.cpp
-rw-rw-r-- 1 lisihan lisihan 176 Jun 2 20:47 makefile
结果表明这个函数可以实现程序替换,用我们自己写的code5来执行ls -a -l
命令。
替换原理
⽤fork创建⼦进程后执⾏的是和⽗进程相同的程序(但有可能执⾏不同的代码分⽀),⼦进程往往要调⽤⼀种exec函数以执⾏另⼀个程序。当进程调⽤⼀种exec函数时,该进程的⽤⼾空间代码和数据完全被新程序替换,从新程序的启动例程开始执⾏。调⽤exec并不创建新进程,所以调⽤exec前后该进程的id并未改变。
重点:
-
进程程序替换没有创建子进程。只是将这个进程的PCB中的虚拟内存通过页表映射的物理内存中的代码段和数据段替换成了另一个程序的代码和数据。由于进程中原有的代码和数据都被替换了,所以原来程序后面的代码就不会再运行了
-
进程程序替换不仅可以执行系统的程序,同样可以执行我们自己创建的可执行程序
-
调⽤exec前后该进程的id并未改变,这个同样我们可以证明一下
//code5.cpp #include <iostream> #include <unistd.h> int main() {printf("my pid: %d, ppid: %d\n", getpid(), getppid());execl("./code3", "code3", nullptr); return 0; } //code3.c #include <stdio.h> #include <string.h> #include <errno.h> #include <stdlib.h> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> int main() {pid_t id = fork();if(id < 0){printf("errno : %d, errstring: %s\n", errno, strerror(errno));return errno;}else if(id == 0){int cnt = 3;while(cnt){printf("子进程运行中, pid: %d\n", getpid());cnt--;sleep(1);}exit(123);}else{sleep(20);int status = 0;pid_t rid = waitpid(id, &status, 0); // == waitif(rid > 0)printf("wait sub process success, rid: %d, status code: %d, singal: %d\n", rid, status >> 8 & 0xFF, status & 0x0F );elseperror("waitpid");while(1){printf("我是父进程: pid:%d\n", getpid());sleep(1);}}return 0; }
运行结果:
[lisihan@hcss-ecs-b735 lession15]$ ./code5my pid: 29566, ppid: 28461子进程运行中, pid: 29567子进程运行中, pid: 29567子进程运行中, pid: 29567子进程运行中, pid: 29567子进程运行中, pid: 29567wait sub process success, rid: 29567, status code: 123, singal: 0我是父进程: pid:29566我是父进程: pid:29566我是父进程: pid:29566我是父进程: pid:29566我是父进程: pid:29566
如果想要进程程序替换但是自己本身又要执行后面的代码,我们可以利用
fork()
函数创建一个子进程来进程程序替换,自己的进程继续运行后续的代码,具体就不演示了。其实这与我们shell的功能有一些类似,shell也是一个进程,这个进程接收到用户输入会fork一个子进程,然后根据用户输入execl其他程序 -
从进程的程序替换我们可以知道,进程的数据和代码也可以发生写时拷贝,此时进程就是彻底独立的
进程程序替换的接口——exec替换函数
所有函数:
include <unistd.h>
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 execve(const char *path, char *const argv[], char *const envp[]);
这些函数的不同点在于传参方式的不同,根据函数名可以分为以下传参方式,这些函数除了execve
是系统调用函数,其他函数都是C语言提供的库函数,这些库函数都是复用execve
函数实现的:
l(list) : 表⽰参数采⽤列表
v(vector) : 参数⽤数组
p(path) : 有p⾃动搜索环境变量PATH
e(env) : 表⽰⾃⼰维护环境变量
这个表中如果带路径表示用户不需要自己写路径,对于环境变量也是一样的
- 这些函数如果调⽤成功则加载新的程序从启动代码开始执⾏,不再返回。
- 如果调⽤出错则返回-1
- 所以exec函数只有出错的返回值⽽没有成功的返回值。
- 在使用列表类函数的时候传参的最后一个要加上nullptr