进程控制
fork函数
创建一个子进程, 原型:
fork确实创建了一个子进程并完全复制父进程,但是子进程是从fork后面那个指令开始执行的。
| 12
 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
 
 | #include <stdio.h>#include <unistd.h>
 #include <fcntl.h>
 #include <stdlib.h>
 
 
 int main()
 {
 pid_t pid;
 printf("sdsfdfdsfd\n");
 pid = fork();
 if(pid == -1)
 {
 perror("fork");
 exit(1);
 }
 else if(pid == 0)
 {
 printf("I'm child, pid =%u,ppid = %u\n",getpid(),getppid());
 }
 else
 {
 printf("I'm Parent, pid =%u,ppid = %u\n",getpid(),getppid());
 }
 printf("yyyyyyy\n");
 return 0;
 }
 
 | 
创建n个线程:
| 12
 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
 30
 31
 32
 33
 34
 35
 
 | #include <stdio.h>#include <stdlib.h>
 #include <unistd.h>
 
 int main()
 {
 int i;
 pid_t pid;
 printf("xxxxxxx\n");
 
 for(i=0; i<5 ;i++)
 {
 pid = fork();
 if(pid == -1)
 {
 perror("fork");
 exit(1);
 }
 else if(pid == 0)
 {
 break;
 }
 }
 if(i < 5)
 {
 sleep(i);
 printf("I'm %d child, pid = %u\n",i+1, getpid());
 }
 else
 {
 sleep(i);
 printf("I'm parent\n");
 }
 return 0;
 }
 
 | 
父进程和子进程谁先执行?谁先抢到cpu就谁先执行。如果不加sleep,则输出是乱序的(反映了操作系统对进程调度的无序性)。
getuid
getgid
进程共享
父子进程之间在fork后,有哪些相同,哪些相异?
刚fork之后:
父子相同之处:全局变量、.data段,.text段,栈,堆,环境变量,用户ID,宿主目录,进程工作目录,信号处理方式…
父子不同之处:进程ID,fork返回值,父子进程ID,进程运行时间,闹钟(定时器),未决定信号集
注意:子进程并不是把父进程0~3G地址空间完全cpoy一份, 然后映射到物理内存。 父子进程之间遵循读时共享写时复制的原则。这样设计,无论子进程执行父进程的逻辑还是执行自己的逻辑都能节省内存开销。
| 12
 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
 30
 
 | #include <stdio.h>#include <stdlib.h>
 #include <unistd.h>
 
 int var = 34;
 int main()
 {
 pid_t pid;
 
 pid = fork();
 if(pid == -1)
 {
 perror("fork");
 exit(1);
 }
 else if(pid > 0)
 {
 sleep(2);
 
 printf("I'm parent pid = %d, parentID = %d, var = %d\n",getpid(), getppid(),var);
 }
 else if(pid == 0)
 {
 var = 100;
 printf("child pid = %d, parentID = %d, var = %d\n",getpid(),getppid(),var);
 }
 printf("var = %d\n",var);
 
 return 0;
 }
 
 | 
重点:
特别:fork之后父进程先执行还是子进程先执行不确定。取决于内核所使用的调度算法。
gdb调试
使用gdb调试的时候,gdb只能跟踪一个进程。可以在fork函数调用之前,通过命令设置gdb调试工具跟踪父进程或是子进程。默认跟踪父进程。
| 12
 
 | set follow-fork-mode child #命令设置gdb在fork之后跟踪子进程set follow-fork-mode parent #设置跟踪父进程
 
 | 
注意:一定要在fork函数调用之前设置才有效。
exec函数族
fork函数创建子进程后执行的是和父进程相同的程序(但可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。
将当前进程的.text, .data替换为所要加载的程序的.text, .data,然后让进程从新的.text第一条指令开始执行,但进程ID不变,换核不换壳。
- int execl()
- int execlp()
- int  execle()
- int execv()
- int execvp()
- int execve()
execlp
execlp中的p表示环境变量, 所以该函数通常用来调用系统程序
| 12
 
 | int execlp(const char* file, const char* arg, ... );
 
 | 
注意结尾加上NULL指定变参结束, printf函数也是变参, 结尾也要加上NULL作为哨兵.
该函数通常用来调用系统程序。如ls, cat , date等命令
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 
 | int main(int argc,char* argv[]){
 pid_t pid=fork();
 if(pid==-1)
 perr_exit("fork error");
 
 if(pid==0){
 
 execlp("ls","ls","-l","-R","-h",NULL);
 
 perror("execlp error");
 exit(1);
 }else if(pid>0){
 printf("I'm parent:%d\n",getpid());
 sleep(1);
 }
 return 0;
 }
 
 | 
先fork, 再exec, 这就是bash的大概原理.
如果要执行自己的可执行文件:
| 1
 | execl("./test","./test",NULL);
 | 
execl
加载一个进程,通过 路径+程序名 来加载。
| 12
 3
 4
 5
 
 | int execl(const char *path, const char\* arg, ...)
 
 execlp("ls","ls","-a","-l",NULL);
 execl("/bin/ls","ls","-a","-l");
 
 | 
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 
 | int main(int argc,char* argv[]){
 
 int ret=0;
 int fd1=0;
 
 fd1=open("ps.log",O_RDWR|O_CREAT|O_TRUNC,0644);
 if(fd1==-1)
 perr_exit("open error");
 
 ret=dup2(fd1,STDOUT_FILENO);
 if(ret==-1)
 perr_exit("dup2 error");
 
 execlp("ps","ps","aux",NULL);
 perror("execlp error");
 exit(1);
 return 0;
 }
 
 | 
exec函数族一般规律
exec函数一旦调用成功即执行新的程序,不返回。只有失败才返回,错误值-1。所以通常直接在exec函数后直接调用perror()和exit(),无需if判断。
事实上,只有execve是真正的系统调用,其他五个函数最终都调用execev,所以execve在man手册第二节,而其他函数在man手册第三节。
回收子进程
孤儿进程
父进程先于子进程结束,则子进程变为孤儿进程,子进程的父进程变为init进程,称为init进程领养孤儿进程。该过程主要是为了后期进行回收。
僵尸进程
进程终止,父进程尚未回收,子进程残留资源(PCB)存放在内核中,变成僵尸(Zombie)进程。
特别注意:僵尸进程不能使用kill命令来清除。因为kill命令只是用来终止进程的,而僵尸进程已经终止。
进程的运行状态:R 运行, S 后台运行, Z 僵尸进程
用什么办法可以清除僵尸进程?避免僵尸进程:回收
wait函数
一个进程在终止的时候会关闭所有的文件描述符,释放在该用户空间分配的内存,但它的PCB还保留着,内核在其中保存了一些信息。如果是正常终止则保存着退出状态,如果是异常终止则保存着导致该进程终止的信号是哪个。这个进程的父进程可以调用wait或者waitpid获取这些信息,然后彻底清除掉这个进程。一个进程的退出状态可以在shell中用特殊变量$?查看,因为shell是它的父进程,当它终止时shell调用wait或waitpid得到它的退出状态同时彻底清除掉这个进程。
父进程调用wait函数可以回收子进程终止信息,该函数的三个功能:
- 阻塞等待子进程退出
- 回收子进程残留资源
- 获取子进程结束状态(退出原因)
| 12
 
 | pid_t wait(int *status);
 
 | 
当进程终止时,操作系统的隐式回收机制会:
- 关闭所有文件描述符
- 释放用户空间分配的内存,内核的PCB仍存在。其中保存该进程的退出状态。(正常终止:退出值;异常终止:终止信号)
可使用wait函数传出参数status来保存进程的退出状态。借助宏函数来进一步判断进程终止的具体原因。宏函数可以分为三组:
| 12
 3
 4
 5
 6
 7
 8
 9
 
 | WIFEXITED(status) WEXITSTATUS(status)
 
 WIFSIGNALED(status)
 WTERMSIG(status)
 
 WIFSTOPED(status)
 WSTOPSIG(status)
 WIFCONTINUED(status)
 
 | 
kill -l 可以查看进程结束的所有状态
程序所有异常终止的原因都是因为信号
| 12
 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
 
 | int main(int argc,char* argv[]){pid_t pid,wpid;
 int status=0;
 
 pid=fork();
 if(pid==-1)
 perr_exit("fork error");
 
 if(pid==0){
 printf("I'm child:%d,my parent is %d,I'm going to sleep 10s\n",getpid(),getppid());
 sleep(10);
 printf("I'm child,I'm going to die\n");
 return 73;
 }else if(pid>0){
 
 wpid=wait(&status);
 if(wpid==-1)
 perr_exit("wait error");
 
 if(WIFEXITED(status))
 printf("My child exited with:%d\n",WEXITSTATUS(status));
 
 else if(WIFSIGNALED(status))
 printf("My child killed by:%d\n",WTERMSIG(status));
 
 printf("I'm parent,wait %d finish\n",wpid);
 }
 return 0;
 }
 
 | 
waitpid函数
作用同wait,但可指定pid进程清理,可以不阻塞
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 
 | pid_t waitpid(pid_t pid, int *status, int options); 
 
 
 
 
 
 
 
 
 
 
 
 
 | 
注意:一次wait或waitpid调用只能清理一个子进程,清理多个子进程应该使用循环。
| 12
 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
 
 | int main(int argc,char* argv[]){
 int i=0;
 int wpid=0;
 int pid=0;
 for(i=0;i<5;++i){
 if(fork()==0){
 if(i==2)
 pid=getpid();
 break;
 }
 }
 
 if(i==5){
 
 sleep(5);
 wpid=waitpid(pid,NULL,WNOHANG);
 if(wpid==-1)
 perr_exit("waitpid error");
 printf("I'm parent,wait a child finish:%d\n",wpid);
 }
 else{
 sleep(i);
 printf("I'm %dth child,my pid=%d\n",i+1,getpid());
 }
 sleep(1);
 return 0;
 }
 
 | 
bug的原因:在fork()==0时是在子进程的执行逻辑中保存了pid, 但是子进程执行结束后直接返回, 用户空间的地址空间被回收, 当然也就没有了pid这个变量, 所以后面父进程waitpid时拿到的pid一直是0。
| 12
 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
 30
 31
 32
 
 | int main(int argc,char* argv[]){
 int i=0;
 int wpid=0;
 int pid=0;
 int temppid=0;
 
 for(i=0;i<5;++i){
 pid=fork();
 if(pid==0)
 break;
 
 if(i==2)
 temppid=pid;
 }
 
 if(i==5){
 
 sleep(5);
 wpid=waitpid(temppid,NULL,WNOHANG);
 if(wpid==-1)
 perr_exit("waitpid error");
 printf("I'm parent,wait a child finish:%d\n",wpid);
 }
 else{
 sleep(i);
 printf("I'm %dth child,my pid=%d\n",i+1,getpid());
 }
 
 sleep(1);
 return 0;
 }
 
 | 
回收多个子进程: 用while循环
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 
 | int main(int argc,char* argv[]){int i=0;
 int wpid=0;
 int pid=0;
 
 for(i=0;i<5;++i){
 pid=fork();
 if(pid==0)
 break;
 }
 
 if(i==5){
 
 while((wpid=waitpid(-1,NULL,WNOHANG))!=-1){
 if(wpid>0)
 printf("wait chile:%d\n",wpid);
 else if(wpid==0)
 sleep(1);
 }
 }else{
 sleep(i);
 printf("I'm %dth child,my pid=%d\n",i+1,getpid());
 }
 sleep(1);
 return 0;
 }
 
 | 
wait/waitpid只能回收子进程, 爷孙的也不行.