进程创建后,需要对其进行合理管理,光靠OS 是无法满足我们的需求的,此时可以运用进程控制相关知识,对进程进行手动管理,如创建进程、终止进程、等待 进程等,其中等待进程可以有效解决僵尸进程问题。
1、进程创建
1.1、fork函数
#include <unistd.h> //所需头文件 pid_t fork(void); //fork 函数
看如下代码:
#include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> //进程等待相关函数头文件 int main() { //创建两个子进程 pid_t id1 = fork(); if(id1 == 0) { //子进程创建成功,创建孙子进程 pid_t id2 = fork(); if(id2 == 0) { printf("我是孙子进程,PID:%d PPID:%d\n", getpid(), getppid()); exit(1); //孙子进程运行结束后,退出 } wait(0); //等待孙子进程运行结束 printf("我是子进程,PID:%d PPID:%d\n", getpid(), getppid()); exit(1); //子进程运行结束后,退出 } wait(0); //等待子进程运行结束 printf("我是父进程,PID:%d PPID:%d\n", getpid(), getppid()); return 0; //父进程运行结束后,退出 }
可以得出结论:两个子进程已经创建成功,但最晚创建的进程总是最先运行。先执行的哪个进程取决于调度器
1.2 写时拷贝
写时拷贝机制实现原理就是通过页表+MMU机制,对不同进程进行空间寻址,达到出现改写行为时,父子进程使用不同空间地址的效果。
#include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> //进程等待相关函数头文件 const char* ps = "This is a Beauty(美女)"; //全局属性 int main() { pid_t id = fork(); if(id == 0) { ps = "This is a Beast(野兽)"; //改写 printf("我是子进程,我认为:%s\n", ps); exit(0); //子进程退出 } wait(0); //等待子进程退出 printf("我是父进程,我认为:%s\n", ps); return 0; }
运行结果:
可以得出结论:
子进程对ps指针做出改变,父进程不受影响。这就是写时拷贝机制。
2、进程终止
常见的进程退出方式:
1、代码跑完,结果正确
2、代码跑完,结果错误
3、代码没跑完,程序异常了
2.1进程退出码
我们把 main
函数的 return x
返回值称之为 进程退出码。
进程退出码是非常重要的,进程退出码表征了进程推出的信息,它是要给父进程读取的。
我们通过内置命令 echo
,我们让 自己执行内部的函数来打印。
$ echo $?
看如下代码:
表示代码运行过后result不为100,所以return 11。
2.2 进程错误码
首先,失败的非零值是可以自定义的,我们可以看看系统对于不同数字默认的 错误码 是什么含义。C 语言当中有个的 string.h``
中有一个 strerror
接口,是最经典的、将错误码表述打印出来的接口,
代码:
#include <stdio.h> #include <string.h> int main(void) { int i = 0; for (i = 0; i < 1000; i++) { printf("%d: %s\n", i, strerror(i)); } }
运行结果:
一共有133个错误码,错误码退出码可以对应不同的错误原因,方便我们定位问题出在哪里。
2.3退出方式
对一个正在运行中的进程,存在两种终止方式:外部终止和内部终止,外部终止时,通过 kill -9 PID
指令,强行终止正在运行中的程序,或者通过 ctrl + c
终止前台运行中的程序
内部终止是通过函数 exit()
或 _exit()
实现的 之前在程序编写时,发生错误行为时,可以通过 exit(-1)
的方式结束程序运行,代码中任意地方调用此函数,都可以提前终止程序
void exit(int status); void _exit(int status);
来看看exit()与_exit()的区别:
使用 _exit()
时,并没有任何语句输出
3、进程等待
3.1为什么要进程等待
子进程运行结束后,父进程没有等待并接收其退出码和退出状态,OS无法释放对应的内核数据结构+代码数据,出现僵尸进程。
为了避免这种情况的出现,父进程可以通过函数等待子进程的运行结束,此时父进程属于阻塞状态。进程等待也可以获取子进程的执行结果。
3.2 等待函数
系统提供的父进程等待函数有两个 wait()
和 waitpid()
,后者比较常用
#include <sys/types.h> #include <sys/wait.h> pid_t wait(int* status); pid_t waitpid(pid_t pid, int* status, int options);
status 位图结构
wait 和 waitpid,都有一个 status 参数,该参数是一个输出型参数,由操作系统填充;
如果传递NULL,表示不关心子进程的退出状态信息;否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程;
status 不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究 status 低16比特位):
可以看到,status 低两个字节的内容被分成了两部分 – 第一个字节前七位表示退出信号,最后一位表示 core dump 标志;第二个字节表示退出状态,退出状态即代表进程退出时的退出码;
对于正常退出的程序来说,退出信号和 core dump 标志都为0,退出状态等于退出码;对于异常终止的程序来说,退出信号为不同终止原因对应的数字,退出状态未用,无意义。
wait函数的使用
#include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <sys/wait.h> #include <unistd.h> int main(){ int id=fork(); if(id == 0) { //子进程 int cnt = 5; while(cnt--) { printf("子进程, pid:%d, ppid:%d, cnt:%d\n", getpid(), getppid(), cnt); sleep(1); } exit(1); } else { //父进程 sleep(10); int status = 0; pid_t ret = wait(&status); printf("exit code:%d\n",status); } return 0; }
运行结果可知:可以看到,最开始父子进程都处于睡眠状态 S,之后子进程运行5s退出,此时由于父进程还要休眠5s,所以没有对子进程进行进程等待,所以子进程变成僵尸状态 D;5s过后,父进程使用 wait 系统调用对子进程进行进程等待,所以子进程由僵尸状态变为彻底死亡状态。
waitpid函数使用
我们也可以用 waitpid 来进行进程等待:
头文件:sys/types.h sys/wait.h 函数原型:pid_t waitpid(pid_t pid, int *status, int options); pid:Pid=-1,等待任意一个子进程,与wait等效;Pid>0.等待其进程id与pid相等的子进程; status:输出型参数,获取子进程退出状态,不关心则可以设置成为NULL; options:等待方式,options=0,阻塞等待;options=WNOHANG,非阻塞等待; 返回值:waitpid调用成功时返回被等待进程的pid;如果设置了WNOHANG,且waitpid发现没有已退出的子进程可收集,则返回0;调用失败则返回-1;
waitpid代码举例:
#include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <sys/wait.h> #include <unistd.h> int main(){ int id=fork(); if(id == 0) { //子进程 int cnt = 5; while(cnt--) { printf("子进程, pid:%d, ppid:%d, cnt:%d\n", getpid(), getppid(), cnt); sleep(1); } exit(1); } else { //父进程 sleep(10); int status = 0; pid_t ret = waitpid(id,&status,0); printf("exit signal:%d, exit code:%d\n", (status & 0x7f), (status >> 8 & 0xff)); } return 0; }
printf("exit signal:%d, exit code:%d \n", (status & 0x7f), (status >> 8 & 0xff));其中,status 按位与上 0x7f 表示保留低七位,其余九位全部置为0,从而得到退出信号;
status 右移8位得到退出状态,再按位与上 0xff 是为了防止右移时高位补1的情况;
可以看到,waitpid 和 wait 还是有很大区别的 – waitpid 可以传递 id 来指定等待特定的子进程,也可以指定 options 来指明等待方式。
阻塞与非阻塞等待
waitpid 函数的第三个参数用于指定父进程的等待方式:
阻塞:顾名思义,就是进程或是线程执行到这些函数时必须等待某个事件的发生,如果事件没有发生,进程或线程就被阻塞,函数不能立即返回。 非阻塞:就是进程或线程执行此函数时不必非要等待事件的发生,一旦执行肯定返回,以返回值的不同来反映函数的执行情况,如果事件发生则与阻塞方式相同,若事件没有发生则持续返回一个值来告知事件未发生,进程或线程继续执行,直到事件发生才为最后一次返回。
WIFEXITED 与 WEXITSTATUS 宏
Linux 提供了 WIFEXITED 和 WEXITSTATUS 宏 来帮助我们获取 status 中的退出状态和退出信号,而不用我们自己去按位操作:
WIFEXITED (status):若子进程正常退出,返回真,否则返回假;(查看进程是否是正常退出)(wait if exited) WEXITSTATUS (status):若 WIFEXITED 为真,提取子进程的退出状态;(查看进程的退出码)(wait exit status)
if(WIFEXITED(status)) { //正常退出 printf("exit code:%d\n", WEXITSTATUS(status)); } else { //异常终止 printf("exit signal:%d\n",WIFEXITED(status)); }
非阻塞等待与宏的代码展示:
#include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <sys/wait.h> #include <unistd.h> void task1() { printf("task is running...\n"); } void task2() { printf("task is runnning...\n"); } int main() { int id = fork(); if(id == 0) { //子进程 int cnt = 5; while(cnt--) { printf("子进程, pid:%d, ppid:%d, cnt:%d\n", getpid(), getppid(), cnt); sleep(1); } exit(1); } else { //父进程 int status = 0; while(1) { //轮询 pid_t ret = waitpid(id, &status, WNOHANG); //非阻塞式等待 if(ret == 0){ //调用成功,但子进程未退出 printf("等待成功,但子进程未退出\n"); task1(); //执行其他命令 task2(); } else { //调用成功,子进程退出 printf("等待成功,子进程退出\n"); break; } sleep(1); } if(WIFEXITED(status)) { //正常退出 printf("exit code:%d\n", WEXITSTATUS(status)); } else { //异常终止 printf("exit signal:%d\n",WIFEXITED(status)); } } return 0; }
代码展示结果可以看出:程序正常运行,使用waitpid的非阻塞等待,父进程通过等待轮询的方式,父进程在等待子进程的过程中,也可以执行其他任务。
4、进程程序替换
在上面进程创建中我们提到,fork 函数一般有两种用途 – 创建子进程来执行父进程的部分代码以及创建子进程来执行不同的程序,创建子进程来执行不同的程序就是进程程序替换。如果想让子进程和父进程彻底分开,让子进程彻彻底底地执行一个全新的程序,我们就需要 进程的程序替换。
为什么要进行程序替换?因为我们想让我们的子进程执行一个全新的程序。
那为什么要让子进程执行新的程序呢?
我们一般在服务器设计的时候(Linux 编程)的时候,往往需要子进程干两件种类的事情:
-
让子进程执行父进程的代码片段(服务器代码…)
-
想让子进程执行磁盘中一个全新的程序(shell、想让客户端执行对应的程序、通过我们的进程执行其他人写的进程代码、C/C++ 程序调用别人写的 C/C++/Python/Shell/Php/Java...)
5、七大进程替换函数
进程替换函数一共有七个,其中六个都在调用execve。execve才是真正的系统级接口
这些函数都属于exec家族的替换函数,这些函数在程序替换失败后才有返回值,返回值为-1。程序都已经替换成功,后续代码也都将被替换,所以成功后的返回值也就没意义了
5.1函数execl
#include<unistd.h> int execl(const char* path, const char& arg, ...);
如果我们想执行一个全新的程序,我们需要做几件事情:
(要执行一个全新的程序,以我们目前的认识,程序的本质就是磁盘上的文件)
-
第一件事情:先找到这个程序在哪里。
-
第二件事情:程序可能携带选项进行执行(也可以不携带)。
明确告诉 OS,我想怎么执行这个程序?要不要带选项。
简单来说就是:① 程序在哪? ② 怎么执行?
所以,execl的接口就把这两个功能给体现出来了
它的第一个参数是 path,属于路径。
参数 const char* arg, ... 中的 ... 表示可变参数,命令行怎么写(ls, -l, -a) 这个参数就怎么填。ls, -l, -a 最后必须以 NULL 结尾,表示 "如何执行程序的" 参数传递完毕。
代码展示:
#include<stdio.h> #include<unistd.h> int main(){ printf("我是一个进程,我的PID是:%d\n", getpid()); // ls -a -l int ret=execl("/usr/bin/ls", "ls", "-l", "-a", NULL); // 带选项 //程序替换多发生于子进程,也可以通过子进程的退出码来判断是否替换成功 if(ret == -1) printf("程序替换失败!\n"); printf("我执行完毕了,我的PID是:%d\n", getpid()); return 0; }
结果展示:
可以看出:函数execl中的命令+选项+NULL是以链表的方式进行传递的。
5.2函数execv
#include <unistd.h> int execv(const char* path, char* const argv[]);
path 参数和 execl 一样,关注的都是 "如何找到"
argv[] 参数关注的是 "如何执行",是个指针数组,放 char* 类型,指向一个个字符串。
大家在命令行上 $ ls -a -l ,在 execl 里我们是这么传的: "ls", "-a", "-l", NULL 。
所以 execv 和 execl 只有传参方式的区别,一个是可变参数列表 (l),一个是指针数组 (v)。
返回值:替换失败返回
-1
参数1:待替换程序的路径,如
/usr/bin/ls
参数2:待替换程序名及其命名构成的
指针数组
,相当于一张表
代码展示:
#include<stdio.h> #include<unistd.h> int main(){ printf("我是一个进程,我的PID是:%d\n", getpid()); // ls -a -l char* const argv[]={ "ls","-a","-l",NULL };//arvg表,实际为指针数组 execv("/usr/bin/ls",argv); printf("我执行完毕了,我的PID是:%d\n", getpid()); return 0; }
可以看出:展示结果和之前一样。
5.3函数execlp
可能写path路径过于麻烦了,我们可以换成写文件名,比如说写ls,它就会自动帮我们找到对应的路径。所以这一块的参数传递,和 execl 是一样的,唯一的区别是比 execl 多了一个 p!
我们执行指令的时候,默认的搜索路径在环境变量 中,所以这个 p 的意思是环境变量。
这意味着:执行 execlp 时,会直接在环境变量中找,不用去输路径了,只要程序名即可。
#include <unistd.h> int execlp(const char* file, const char* arg, ...);
返回值:替换失败返回
-1
参数1:待替换程序名,如
ls
、pwd
、clear
参数2~N:可变参数列表,为命令的选项
代码展示:
#include <stdio.h> #include <stdlib.h> //exit 函数头文件 #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> int main(){ //execlp函数 printf("我是父进程,我的PID是: %d\n", getpid()); pid_t id=fork(); if(id==0){ printf("我是子进程,我的PID是: %d\n",getpid()); execlp("ls","ls","-a","-l",NULL);//程序替换 printf("还可以调用吗?"); exit(-1); } int status = 0; waitpid(id, &status, 0); //等待阻塞 if(WIFEXITED(status)) { //正常退出 printf("子进程替换成功,exit code:%d\n", WEXITSTATUS(status)); } else { //异常终止 printf("子进程异常终止,exit signal:%d\n",WIFEXITED(status)); } return 0; }
结果展示:
程序替换在子进程替换只会影响调用的子进程,不会影响父进程。
5.4函数execvp
int execvp(const char* file, char* const argv[]);
代码展示:
#include <stdio.h> #include <stdlib.h> //exit 函数头文件 #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> int main(){ //execlp函数 printf("我是父进程,我的PID是: %d\n", getpid()); pid_t id=fork(); if(id==0){ char* const argv[] = { "ls", "-a", "-l", NULL }; printf("我是子进程,我的PID是: %d\n",getpid()); execvp("ls",argv);//程序替换 printf("还可以调用吗?"); exit(-1); } int status = 0; waitpid(id, &status, 0); //等待阻塞 if(WIFEXITED(status)) { //正常退出 printf("子进程替换成功,exit code:%d\n", WEXITSTATUS(status)); } else { //异常终止 printf("子进程异常终止,exit signal:%d\n",WIFEXITED(status)); } return 0; }
5.5函数execle
e表示env环境变量表,可以将自定义或当前程序中的环境变量表传给待替换程序。
#include <unistd.h> int execl(const char* path, const char* arg, ..., char* const envp[]);
-
最后一个参数:替换成功后,待替换程序的环境变量表,可以自定义
makefile知识点补充:
我们前几章写的 Makefile 文件只能形成一个可执行程序,现在我们学习如何形成多个。
比如,如果我们想一口气形成 2 个 可执行程序:
假设有两个可执行程序:mycmd.cpp & mytest.c,我们期望用 mytest.c 调用 mycmp.cpp:
也就是 C 语言的可执行程序调用 C++ 的可执行程序,我们先来设计一下 Makefile。
我们需要在前面添加 .PHONY:all ,让伪目标 all 依赖 mytest 和 mycmd。
如果不这样做,直接写,默认生成的是 mycmd,轮不到后面的 mytest,属于 "先到先得"。
且 Makefile 默认也只能形成一个可执行程序,想要形成多个就需要用到 all 了。
.PHONY:all all:mytest mycmd mytest:mytest,c gcc -o $@ $^ mycmd:mycmd.cpp g++ -o $@ $^ .PHONY:clean clean: rm -f mytest mycmd
先写一个cpp的代码:
#include<iostream> using namespace std; extern char** environ; //声明环境变量表 int main(){ int pos = 0; //只打印5条 while(environ[pos] && pos < 5) { cout << environ[pos++] << endl; } return 0; }
编译结束后看它所在的路径:
再编写用进程函数替换execle代码:
#include <stdio.h> #include <stdlib.h> //exit 函数头文件 #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> int main(){ //execlp函数 extern char** environ; // 环境变量的指针声明 printf("我是父进程,我的PID是: %d\n", getpid()); pid_t id=fork(); if(id==0){ printf("我是子进程,我的PID是: %d\n",getpid()); execle("./mycmd","mycmd", NULL, environ);//程序替换 printf("还可以调用吗?"); exit(-1); } int status = 0; waitpid(id, &status, 0); //等待阻塞 if(WIFEXITED(status)) { //正常退出 printf("子进程替换成功,exit code:%d\n", WEXITSTATUS(status)); } else { //异常终止 printf("子进程异常终止,exit signal:%d\n",WIFEXITED(status)); } return 0; }
这不就可以成功替换了:
也可以使用自定义的环境变量:
5.6函数execve
#include <unistd.h> int execve(const char* filename, char* const argv[], char* const envp[]);
execle 参数传递是参数列表,execve 参数传递是数组,仅此而已。
5.7函数execvpe
#include <unistd.h> int execvpe(const char* file, char* const argv[], char* const envp[]);
返回值:替换失败返回
-1
参数1:待替换程序名,需要位于
PATH
中参数2:待替换程序名及其命名构成的
指针数组
参数3:传递给待替换程序的环境变量表
最后分享一波记忆方法: