一.学习进程创建, 终止,等待(使用代码实现)
1.进程创建
(1)fork创建进程
fork调用格式
#include <unistd.h>
pid_t pid = fork();
返回值:子进程返回0,父进程返回子进程的pid,创建失败返回-1
#include <stdio.h> #include <unistd.h> #include <stdlib.h> int main() { pid_t pid; printf("before: pid is %d\n",getpid()); if((pid = fork()) == -1)//fork()子进程返回0,父进程返回子进程pid { perror("fork"); exit(1); } printf("after:pid is %d,fork return is %d\n",getpid(),pid); sleep(1); return 0; }
(2)vfork创建进程
fork与vfork使用区别:
1)vfork创建的子进程与父进程共享地址空间,fork的子进程具有独立的地址空间;
2)vfork保证子进程先运行,在它调用exec或exit之后父进程才可能被调度。
#include <stdio.h> #include <unistd.h> #include <stdlib.h> int main() { int flag = 100; pid_t pid = vfork(); if(pid == -1) { perror("vfork()"); exit(1); } if(pid == 0)//child { flag = 200; printf("child flag is %d\n",flag); exit(0); } else//parent { printf("parent flag is %d\n",flag); } return 0; }
可以看见:子进程改变了父进程的变量值,因为子进程在父进程的地址空间中运行。
#include <stdio.h> #include <unistd.h> #include <stdlib.h> int main() { int flag = 100; pid_t pid = fork(); if(pid == -1) { perror("fork()"); exit(1); } if(pid == 0)//child { flag = 200; printf("child flag is %d\n",flag); exit(0); } else//parent { sleep(1); printf("parent flag is %d\n",flag); } return 0; }
可以发现:fork创建的子进程不能改变父进程的变量值,因为子进程有独立的地址空间。
2.进程终止
(1)进程退出的三种场景
代码运行完毕,正常正确;
代码运行完毕,结果不正确;
代码异常提出。
(2)进程常见的退出方法
1)正常终止(可以用echo $?查看进程退出码)
从main函数返回;
调用exit;
_exit;
2)异常退出
ctrl+c 信号终止
(3)从main函数返回
#include <stdio.h> int main() { printf("hi\n"); return 0; }运行该程序,查看程序退出码为0
将main函数中的0改为4,再运行
可以得到结论:main函数中的return语句返回的即为程序的退出码,其他函数中的return语句返回值并不是;echo $?仅能查看最近一条命令的退出码;退出码0表示程序正常退出结果正确。
(4)调用_exit函数
#include <unistd.h>
void _exit ( int status );
参数:status定义了进程的中止状态,父进程通过wait来获取该值
注:虽然status是int,但是仅有低8位可以被父进程所用。所以_exit(-1)时,在终端查看退出码是255
#include <stdio.h>#include <unistd.h>void test(){ _exit(1);}int main(){ printf("hi");
test(); return 0;}
运行结果为
我们可以知道:_exit函数会直接中止程序,无论该函数出现在调用函数还是主函数中
(5)调用exit函数
#include <unistd.h>
void exit ( int status );
exit函数最后也会调用_exit函数,但在调用_exit函数之前,还会做其他工作( 比如刷新缓冲区 ),所以它其实不是直接中止程序的。
#include <stdio.h> #include <stdlib.h> void test() { exit(1); } int main() { printf("hi"); test(); return 0; }
运行程序,会发现同样的代码,只是将_exit换成了exit。printf中的语句就被输出来了
执行renturn n 等同于执行exit(n),因为调用main的运行时函数会将main的返回值当做exit的参数。
3.进程等待
进程等待的方式一般有两种:
阻塞式等待:子进程没有退出时,父进程一直等着子进程退出(常见于调用函数时)
非阻塞式等待:采取轮询式访问,条件不满足时,父进程会返回去做其他事
(1)wait方法
调用方法:
#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int* status);
返回值:成功返回被等待进程pid,失败返回-1
参数status:输出型参数,获取子进程退出状态,不关心子进程的退出原因时可设置为NULL
#include <stdio.h> #include <sys/types.h> #include <sys/wait.h> #include <unistd.h> #include <stdlib.h> //exit int main() { pid_t id = fork(); if(id == 0)//child { int count = 5; while(count--) { printf("heelo %d,pid is %d\n",count,getpid()); sleep(2); } } else if(id > 0)//father { int st; int ret = wait(&st); if(ret>0 && (st&0X7F)==0)//正常退出(status参数的低8位未收到任何信号) { printf("child exit code:%d\n",(st>>8)&0XFF);//让st的次低8位与8个比特位均为1按位与,得到退出码 } else if(ret > 0) { printf("sig code:%d\n",st&0X7F);//让st的低7位分别与1按位与,得到终止信号 } } else { perror("fork"); exit(1); } return 0; }
当让程序自己执行完成,程序正常退出,收到0号退出码。
当在另一个终端kill该进程,程序异常退出,收到15号SIGTERM程序结束信号。与SIGKILL不同的是该信号可以被阻塞和处理,通常用来要求程序正常退出。
下面测试的是,利用9号信号杀死该进程,程序异常退出,收到9号信号。
(2)waitpid方法
调用方法:
pid_t waitpid ( pid_t pid, int* status, int options );
参数 :
pid : pid = -1,等待任一个子进程,与wait等价;
pid > 0,等待进程ID与pid相等的子进程;
status : WIFEXITED (status):若为正常终止子进程的返回状态,则为真;(查看进程是否正常退出)
WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码;(查看进程退出码)
options :(默认为0)
WNOHANG:若pid指定的子进程没有结束,则waitpid( )函数返回0,不予以等待。若正常结束,则返回该子进程的pid。
当正常返回时,waitpid返回收集到的子进程的pid;
如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在。
#include <stdio.h> #include <unistd.h>//fork #include <sys/types.h> #include <sys/wait.h> #include <stdlib.h> int main() { pid_t id = fork(); if(id < 0) { perror("fork"); return 1; } else if(id == 0)//child { printf("child is run,pid is %d\n",getpid()); sleep(5); exit(3); } else//parent { int status = 0; pid_t ret = waitpid(-1,&status,0);//阻塞式等待 printf("I am waiting\n"); if(WIFEXITED(status) && ret == id)//WIFEXITED(status)为真且正常返回才进入该条件语句 { printf("wait success,child return code is %d\n",WEXITSTATUS(status));//WEXITSTATUS(status))查看进程的退出码 } else { printf("wait child failed,return \n"); return 2; } } return 0; }让程序运行,自己退出。因为子进程用exit(3)退出,所以SEXITSTATUS(status)非零,提取出子进程的退出码3。
当我在另一个终端下,kill掉该进程,子进程异常退出,所以WIFEXITED(status)不为真,所以父进程中走else语句,输出结果如下:
以下是waitpid函数的非阻塞式等待实现:
#include <stdio.h> #include <unistd.h>//fork #include <sys/types.h> #include <sys/wait.h> #include <stdlib.h> int main() { pid_t id = fork(); if(id < 0) { perror("fork"); return 1; } else if(id == 0)//child { printf("child is run,pid is %d\n",getpid()); sleep(5); exit(3); } else//parent { int status = 0; pid_t ret = 0; do { ret = waitpid(-1,&status,WNOHANG);//非阻塞式等待 if(ret == 0)//无退出子进程可收集 { printf("child is running\n"); } sleep(1); }while(ret == 0);//无退出子进程时,会一直询问(轮询式访问),这里1s询问一次 if(WIFEXITED(status) && ret == id)//WIFEXITED(status)为真且正常返回才进入该条件语句 { printf("wait success,child return code is %d\n",WEXITSTATUS(status));//WEXITSTATUS(status))查看进程的退出码 } else { printf("wait child failed,return \n"); return 2; } } return 0; }
子进程正常退出结果
换终端将子进程异常终止,运行结果如下
(3)获取子进程status
1)wait和waitpid,均有一个status参数,该参数是一个输出型参数,由操作系统填充;
2)若传NULL,表示不关心子进程的退出状态信息;
3)否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程;
4)status不能当做简单整型看待,可以当做位图来看待,具体如下图(这里只有低16位比特位):
由图可知,当程序正常终止时,status的位图的低8位不会收到任何信号,保持全0状态,此时位图的次低8位则保存着程序的退出码。一旦低8位收到信号,则表示程序被信号杀死即异常终止。此时位图的低8位保存程序的终止信号,次低8位不会用。不过需要注意的是,实际上用来保存收到终止信号的只有7个比特位,因为低8位中有一个比特位是core dump标志。
二.编写自主shell
实现代码如下:
#include <stdio.h> #include <unistd.h> #include <string.h> #include <ctype.h> //isspace int main() { char buf[1024] = {}; while(1) { pid_t id = fork(); if(id == 0)//child { char* my_arg[32]; printf("myshell>"); fgets(buf,sizeof(buf),stdin); buf[strlen(buf)-1] = 0; char* p = buf; int i = 1; my_arg[0] = buf; while(*p) { if(isspace(*p)) { *p = 0; p++; my_arg[i++] = p; } else { p++; } } my_arg[i] = NULL; execvp(my_arg[0],my_arg); } else { int status = 0; pid_t ret = waitpid(id,&status,0); if(ret > 0) { //printf("%d\n",status&0X7F); } else { printf("waitpid runnning error\n"); } } } p++; my_arg[i++] = p; } else { p++; } } my_arg[i] = NULL; execvp(my_arg[0],my_arg); } else { int status = 0; pid_t ret = waitpid(id,&status,0); if(ret > 0) { //printf("%d\n",status&0X7F); } else { printf("waitpid runnning error\n"); } } } p++; my_arg[i++] = p; } else { p++; } } my_arg[i] = NULL; execvp(my_arg[0],my_arg); } else { int status = 0; pid_t ret = waitpid(id,&status,0); if(ret > 0) { //printf("%d\n",status&0X7F); } else { printf("waitpid runnning error\n"); } } } return 0; }
运行结果如下:
但是代码还是有缺陷,输入命令若是输错时,不能删除,只能重新输入。
三.封装fork/wait等操作,编写函数process_create ( pid_t* pid, void* func, void* arg ),func回调函数就是子进程执行的入口函数,arg是传递给func回调函数的参数
实现代码如下:
#include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <sys/types.h> #include <sys/wait.h> void process_create(int(*func)(), const char* file, char* argv[]) { int ret = 0; pid_t pid = fork(); if(pid == 0)//child { ret = func(file,argv); if(ret == -1) { perror("func"); exit(1); } } else { int st; pid_t ret = wait(&st); if(ret == -1) { perror("wait"); exit(2); } } } int main() { char* argv1[] = {"ls"}; char* argv2[] = {"ls","-l","/home/may/Code/Linux/class4",0}; process_create(execvp,*argv1,argv2); return 0; }
运行结果如下:
四.调研popen/system,理解这两个函数与fork的区别
1. popen函数
(1)函数原型
创建一个管道用于进程间通信,并调用shell,因为管道被定义为单向的。所以 type 参数只能定义成只读或者只写, 结果流也相应的是只读或只写。
(2)函数功能
popen()会调用fork()产生子进程,然后从子进程中调用/bin/sh -c来执行参数command的指令。这个进程必须由pclose关闭。
(3)参数
command :
一个字符串指针, 指向一个以NULL为结束符的字符串。 这个字符串包含一个shell命令, 这个命令被送到 /bin/sh 以-c参数 执行, 即由 shell 来执行。
type :
一个指向以NULL为结束符的字符串指针,用“r”代表读,“w“代表写。依照type值, popen( )会建立管道连到子进程的标准输出设备或标准输入设备,然后返回一个文件指针。随后进程便可利用此文件指针来读取子进程的输出设备或是写入到子进程的标准输入设备中。
(4)返回值
成功则返回文件指针,否则返回NULL,错误原因存于errno中。
编写代码如下:(将replace.c文件的内容输出到屏幕)
#include <stdio.h> #include <string.h> #include <stdlib.h> int main() { FILE* file = NULL; char buf[1024] = {0}; file = popen("cat replace.c","r"); if(file == NULL) { perror("popen"); exit(1); } while(fgets(buf,1024,file) != NULL) { fprintf(stdout,"%s",buf); } pclose(file); return 0; }
运行结果如下:
2. system函数
(1)函数原型
(2)函数功能
system( )会调用fork( )产生子进程,由子进程来调用/bin/sh-c string来执行参数string字符串所代表的命令。此命令执行完后随即返回原调用的进程。在调用system( )期间SIGCHLD 信号会被暂时搁置,SIGINT和SIGQUIT 信号则会被忽略。
调用/bin/sh来执行参数指定的命令,/bin/sh 一般是一个软连接,指向某个具体的shell。
实际上system()函数执行了三步操作:
fork一个子进程;
在子进程中调用exec函数去执行command;
在父进程中调用wait去等待子进程结束。
(3)返回值
1)若
exec 执行成功,即 command 顺利执行,则返回 command 通过 exit 或 return 的返回值。(注意 :command 顺利执行不代表执行成功,当参数中存在文件时,不论这个文件存不存在,command 都顺利执行) ;
2)若
exec执行失败,即command没有顺利执行,比如被信号中断,或者command命令根本不存在, 返回 127 ;
3)若
command 为 NULL, 则 system 返回非 0 值;
4)
对于fork失败,system()函数返回-1。
注意:判断一个system函数调用shell是否正常结束的标志是
status != -1;
(WIFEXITED(status))非零且WEXITSTATUS(status) == 0.
编写代码如下:
#include <stdio.h> #include <unistd.h> #include <stdlib.h> int main() { int status = 0; status = system("ls -l"); if(status == -1) { perror("system"); exit(1); } if(WIFEXITED(status) != 0)//正常退出 { if(WEXITSTATUS(status) == 0)//操作正确 { printf("run success\n"); } else { printf("run failed,exit code is %d\n",WEXITSTATUS(status)); } } else//异常退出 { printf("sig code is %d\n",WEXITSTATUS(status)); } }
运行结果如下:
3. 两个函数的区别
(1)
system 在执行期间,调用进程会一直等待 shell 命令执行完成(waitpid),但是 popen 无需等待 shell 命令执行完成就返回。
(2)
popen 函数执行完毕后必须调用 pclose 来对所创建的子进程进行回收,否则会造成僵尸进程的情况。
(3)
open 没有屏蔽 SIGCHLD ,如果在调用时屏蔽了 SIGCHLD ,且在 popen 和 pclose 之间调用进程又创建了其他子进程并调用进程注册了 SIGCHLD 来处理子进程的回收工作,那么这个回收工作会一直阻塞到 pclose 调用。