2、多进程开发
2.1 进程相关命令
-
实施显示进程状态
top
可以在使用top命令加上 -d 来指定显示信息更新的时间间隔,在top执行后,可以按以下按键对显示的结果进行排序: M 根据内存进行排序 P 根据CPU进行排序 T 根据进程运行时间长短排序 K 输入指定的PID杀死进程 U 根据用户名来筛选进程
-
杀死进程
Kill
-
kill -l 列出所有信号
-
kill -9 进程ID 杀死指定ID的进程
-
killall name 根据进程名杀死进程
-
2.2 父子进程
1. 创建子进程
fork()函数
返回值:
父进程中: >0 返回的是子进程的ID
子进程中:=0
注意:fork函数使用的是写时拷贝:一种推迟甚至避免拷贝数据的技术
内核并不复制整个进程地址空间,而且父子共享同一个地址空间。
只在写入的时候才会复制地址空间,从而拥有各自的地址空间。
#include <sys/types.h>
#include <unistd.h>
#include <iostream>
using namespace std;
int main(){
//创建子进程
pid_t pid = fork();
// 判断父进程还是子进程
if(pid>0){
printf("I am parent process,pid: %d, ppid:%d\n",getpid(),getppid());
printf("%d\n",pid);
}else if (pid == 0){
sleep(10);
printf("I am son process,pid:%d\n",getpid());
}
return 0;
}
2.GDB多进程调试
GDB调试的时候,默认只跟踪一个进程(父进程)。
比如:当你在if(为父进程){断点A}else if(子进程){断点B}
默认只会在断点A出停下来,子进程会一直往下执行直到结束。
常见命令:
-
切换默认调试进程:set follow-fork-mode [parent(默认) | child]
-
设置调试模式:set detach-on-fork [on | off]
默认为on,表示调试当前进程的时候,其他进程继续向下执行。
如果为off,则调试当前进程的时候,其他进程会被挂起
-
查看所有调试的进程,以及当前调试的进程:info inferiors
-
切换当前调试的进程:inferior id
-
是进程脱离GDB调试:detach inferiors id
2.3 exec函数族
1.execl()
execl()函数:使用命令man exec查看介绍
-
用法
int execl(const char *pathname, const char *arg, ...); - 参数: - path:需要指定的执行的文件的路径或者名称 a.out /home/wangfan/a.out /bin/ps 执行ps命令 - arg:可执行文件需要的参数列表 第一个参数一般没用,写的是执行程序的名称 从第二个参数开始往后,就是程序执行所需要的参数列表。 注意:参数最好需要以NULL结束,充当哨兵 - 返回值: 只有当调用失败才会有返回值,返回-1,并且设置eoono。 如果调用成功,没用返回值。 - ***功能 注意,当execl()函数执行成功以后,进程后续用户区会被pathname所指定的进程所取代,原本写在execl()后续的代码不会被执行。如果执行失败则后续代码会被执行。
-
测试代码
#include<unistd.h> #include<stdio.h> int main(int argc, char const *argv[]) { // 创建一个子进程,在子进程中执行exec函数族中的函数 pid_t pid = fork(); if(pid>0){ printf("我是父进程 pid = %d, 子进程pid=%d\n",getpid(),pid); }else if(pid == 0){ printf("我是子进程,execl()函数执行前\n"); execl("./hello","hello",NULL);//输出hello,world //execl("/bin/ps","ps","aux",NULL); //执行系统命令 //execl("ps","ps","aux",NULL); /模拟execl()函数执行失败 printf("我是子进程 pid = %d 父进程ppid=%d\n",getpid(),getppid()); //执行成功,该代码不会被执行。 } for(int i=0;i<3;i++){ printf("%d 我的pid = %d\n",i,getpid()); } return 0; }
-
输出结果
2.execlp()
-
用法:同上,p指的是在path环境下查找指定文件
-
代码
#include<unistd.h> #include<stdio.h> int main(int argc, char const *argv[]) { // 创建一个子进程,在子进程中执行exec函数族中的函数 pid_t pid = fork(); if(pid>0){ printf("我是父进程 pid = %d, 子进程pid=%d\n",getpid(),pid); }else if(pid == 0){ printf("我是子进程,execl()函数执行前\n"); //execl("./hello","hello",NULL); execlp("ps","ps_wf","aux",NULL); //执行系统命令 printf("我是子进程 pid = %d 父进程ppid=%d\n",getpid(),getppid()); } for(int i=0;i<3;i++){ printf("%d 我的pid = %d\n",i,getpid()); } return 0; }
-
演示
3.execv()
-
用法
int execv(const char *pathname, char *const argv[]);
与上面的区别在:传递的参数是指针数组
-
代码
#include <stdlib.h> #include <stdio.h> #include <unistd.h> #include <errno.h> int main(void) { char *argv[]={"ls","-l","/home",(char *)0}; pid_t pid = fork(); if( pid == 0 ) // this is the child process { execv("/bin/ls", argv); } return 0; }
-
演示
4.总结
l(list): 参数列表地址,一空指针结尾
v(vector): 指针数组
p(path):按照PATH环境变量指定的目录搜索文件
e(environment):存有环境变量字符串地址的指针数组的地址
2.4 进程控制
1. 进程退出
#include<stdlib.h>
#include<unistd.h>
void exit(int status);//会刷新I/0缓冲区
void _exit(int status);
-
测试代码
#include<stdio.h> #include<stdlib.h> #include<unistd.h> int main(int argc, char const *argv[]) { printf("hello\n"); printf("world"); //exit(0); _exit(0); return 0; }
-
演示
仅有hello,并没有输出world,因为printf没有遇见\n来输出该结果
2. 孤儿进程
父进程运行结束,子进程还在运行,这样的子进程叫做孤儿进程。
当出现孤儿进程的时候,内核会把孤儿进程的父进程设置为init,而init进程会循环地wait()它的已经退出的子进程。这样,当一个孤儿进程结束生命周期的时候,init进程会处理它的善后工作。
因此孤儿进程没有什么危害。
-
代码
#include <sys/types.h> #include <unistd.h> #include <iostream> using namespace std; int main(){ //创建子进程 pid_t pid = fork(); // 判断父进程还是子进程 if(pid>0){ printf("I am parent process,pid: %d, ppid:%d\n",getpid(),getppid()); sleep(1); }else if (pid == 0){ printf("I am son process,pid: %d, ppid:%d\n",getpid(),getppid()); sleep(5); printf("I am son process,pid: %d, ppid:%d\n",getpid(),getppid()); } for(int i=0;i<3;i++){ printf("i = %d,pid = %d\n",i,getpid()); } return 0; }
-
演示
3. 僵尸进程
-
介绍
每个进程结束之后,都会自己是否自己的用户区数据,但是内核区的PCB没有办法回收,只能由父进程释放。如果父进程在子进程之前结束,则子进程的父进程会被设置为init进程,由init进程进行回收。
进程终止时,父进程还没有回收,子进程PCB存放在内核中,变成僵尸进程。
僵尸进程不能被kill -9杀死
僵尸进程会一直占用进程号,但是系统所能使用的进程号是有限的,如果有大量的僵尸进程,那么系统无法产生新的进程,产生大量的危害。
-
代码
#include <sys/types.h> #include <unistd.h> #include <iostream> using namespace std; int main(){ //创建子进程 pid_t pid = fork(); // 判断父进程还是子进程 if(pid>0){ while(1){ printf("I am parent process,pid: %d, ppid:%d\n",getpid(),getppid()); sleep(1); } }else if (pid == 0){ printf("I am son process,pid: %d, ppid:%d\n",getpid(),getppid()); } for(int i=0;i<3;i++){ printf("i = %d,pid = %d\n",i,getpid()); } return 0; }
-
演示
4.进程等待(回收)
-
什么是进程等待
我们知道一般我们在父进程fork出一个子进程,我们是希望子进程完成某些功能,也就是帮助父进程完成某些任务的;所以我们父进程就需要知道子进程完成的状态如何,是成功还是失败;
所以我们就需要父进程通过wait 或者 waitpid 函数等在子进程退出; -
为什么需要父进程等待子进程退出
父进程需要子进程退出的信息和完成功能的状态。
可以保证时序问题:子进程先退出,父进程再退出。以此预防子进程变为僵尸进程,防止内存泄漏的问题。
-
方式
-
wait函数
-
#include<sys/types.h> #include<sys/wait.h> pid_t wait(int*status); /* 返回值: 成功返回被等待进程pid,失败返回-1。 参数: 输出型参数,获取子进程退出状态,不关心则可以设置成为NULL 注意: - 调用wait函数的进程会被挂起(阻塞),直到它的一个子进程退出或者收到一个不能被忽略的信号时才 被唤醒(相当于继续往下执行) - 如果没有子进程了,函数立刻返回,返回-1;如果子进程都已经结束了,也会立即返回,返回-1 */
-
测试代码
#include<string.h> #include<stdio.h> #include<stdlib.h> #include<unistd.h> #include<sys/wait.h> int main() { if(fork() == 0){ //child process printf("i am a child pid=%d\n",getpid()); exit(0); //让子进程退出 } //parent process执行,这里不会执行子进程了,因为子进程被我退出了 sleep(2); //休息2s,为的是观察监控消息,是否子进程成为僵尸进程 printf("wait函数开始执行\n"); pid_t ret = wait(NULL); if(ret ==-1){ perror("wait error\n"); } //wait返回成功 printf("wait返回的是子进程的ret=%d执行结束,注意观察监控窗口是否>僵尸进程被回收\n",ret); sleep(2); //不让父进程那么快退出,观察窗口僵尸进程是否被回收 return 0; }
-
演示结果
-
-
waitpid函数
-
pid_t waitpid(pid_t pid, int *wstatus, int options); - 功能: 回收指定进程号的子进程,可以设置是否阻塞 - 参数: - pid: pid > 0: 某个子进程的pid pid = 0: 等待任何某个子进程的组ID等于调用waitpid()函数的进程的组ID pid = -1: 表示回收任意一个子进程 pid < -1: 表示等待任何进程组ID等于pid绝对值的子进程。 - wstatus:见下文 - opt: 参数为0: 也就是阻塞版本的等待,也就是说该waitpid在子进程没有退出情况下就不会返回,就和wait的使用一模一样,因为wait的使用就是阻塞版本的等待方式; 参数为WNOHANG: 这是一个宏,表示调用wait为非阻塞的版本,非阻塞也就以为执行带waitpid函数会立即返回; 而设置这个参数:返回情况有以下几种: - 若pid指定的子进程没有结束,则waitpid()函数返回0,父进程不予以等待; - 若正常结束,则返回该子进程的ID; - 若等待失败,即返回小于0;
-
-