目录
进程控制
进程控制大概分为 : 进程创建, 进程退出, 进程等待, 程序替换
进程创建
进程创建方式 : fork(), vfork().
fork()
1. 头文件 : #include <unistd.h>
2. 返回值 : pid_t fork(void)
创建成功返回0, 失败返回-1, 父进程返回子进程的pid
创建子进程 :
#include <stdio.h> #include <stdlib.h> #include <errno.h> #include <unistd.h> int main() { pid_t pid = fork(); if(pid< 0) { perror("fork error"); return -1; } else if(pid == 0) { printf("i am child\n"); } else { printf("i am parent\n"); } return 0; }
运行结果 :
我们可以看到, 父进程先运行, 然后子进程才运行,但其实我们用fork()创建出来的子进程和父进程谁先运行是不一定的
vfork()
1. 头文件 : #include <unistd.h>
2. 返回值 : 成功返回0
我们先来看一段代码:
#include <stdio.h> #include <stdlib.h> #include <errno.h> #include <unistd.h> int main() { pid_t pid = vfork(); if(pid == 0) { printf("i am child ---- %d\n",getpid()); } else { printf("i am parent ----- %d\n",getpid()); } while(1) { printf("-------%d\n",getpid()); sleep(1); } return 0; }
运行 :
我们可以看到,vfork()创建的子进程会在父进程之前运行, 这个是一定的, 只有当子进程exit()退出或者使用exec函数族程序替换的时候, 父进程才会开始运行, 我们将上面的代码加一句话 , 在子进程打印完了之后exit, 这个时候父进程就开始运行了:
if(pid == 0) { printf("i am child ---- %d\n",getpid()); exit(0); }
但是不能在main函数中return退出, 这个时候子进程退出释放资源,包括虚拟地址空间里面存的东西,这个时候在centos系统下父进程会陷入一个调用栈混乱的情况, 有可能会重复运行, 那么在Debian系列比如ubantu系统下程序会崩溃,所以一定不要使用main函数中退出的方式退出子进程。
fork 与 vfork的区别
1. fork()创建出来的子进程和父进程谁先运行不一定
2. fork() 创建进程是将父进程的所有数据拷贝一份, 包括虚拟地址空间和页表, 这个时候他们两个里面所有的数据的虚拟地址都是一样的,但是当子进程对一个变量进行修改的时候, 这个时候系统会为这个 变量重新开辟空间, 也就是我们上篇博客中所说的 写时拷贝技术, 子进程与父进程代码共享, 数据独有
3. vfork()创建出来的子进程与父进程公用同一块虚拟地址空间, 这个时候我们在子进程中对数据进行拷贝的时候,父进程中会随着一起改变,有可能会造成函数调用栈混乱, 所以当fork实现了写时拷贝技术之后vfork基本就被淘汰了。
4. vfork存在的意义是快速创建子进程, 因为公用一块虚拟地址空间, 减少了子进程拷贝父进程的消耗, 所以速度快
5. vfork创建出子进程后一定是子进程先运行, 等到子进程exit退出或者exec函数族程序替换之后父进程才会开始运行。
进程终止
进程终止的场景 :
1. 正常退出结果符合预期
2. 正常退出结果不符合预期
3. 异常退出
进程终止有三种方式 :
1. main函数中return
2. exit() exit是库函数接口, 底层也是调用_exit,但是调用前会刷新缓冲区,做退出前的收尾工作
3. _exit() _exit是系统调用接口, 直接退出, 释放资源
1. return
#include <stdio.h> int main() { printf("nihao\n"); return 0; }
调用return其实相当于调用exit(), 因为函数会将main的返回值当做exit的参数
2. exit()
头文件 : #include <stdlib.h>
#include <stdio.h> #include <stdlib.h> int main() { printf("nihao\n"); exit(0); }
3. _exit()
头文件 : #include <unistd.h>
#include <stdio.h> #include <unistd.h> int main() { printf("nihao\n"); _exit(0); }
进程退出返回值
我们通过 : echo $? 查看进程正常退出的返回值
比如:
#include <stdio.h> #include <stdlib.h> int main() { printf("nihao\n"); exit(255); }
获取返回值 :
这里我们还有一个概念叫做 : 错误编号
系统调用完毕都会重置进程中errno这么一个全局变量,这个全局变量中存储的就是当次调用的系统调用接口错误编号,当系统调用接口出错, 用户就可以通过这个errno获取系统调用的错误原因
比如 :
#include <stdio.h> #include <stdlib.h> #include <unistd.h> int main() { //先睡十秒 sleep(10); printf("nihao\n"); exit(255); }
我们Ctrl + c 结束终止进程, 然后获取退出返回值 :
我们可以看到, 返回值并不是255, 而是130, 其实当程序异常退出的时候回返回一个未知数.
进程等待
为什么要进行进程等待?
1. 我们之前在进程概念的章节说过, 如果不进行进程等待, 子进程退出父进程不知道, 会造成僵尸进程, 进而会内存泄漏
2. 僵尸进程无法杀死, kill -9 也不行
3. 父进程需要知道子进程的任务完成的如何, 以及结果是否正确, 然后回收子进程的资源, 回去子进程的退出信息
进程等待的方法
wait 和 waitpid
具体的有阻塞和非阻塞两种 :
阻塞 : 为了完成功能发起调用, 如果当前不具备完成条件, 则一直等待, 直到完成后返回
非阻塞 : 为了完成功能发起调用, 如果当前不具备完成条件, 立即报错返回
1. wait
头文件 :
#include<sys/types.h>
#include<sys/wait.h>
函数 :
pid_t wait(int *status);
返回值 : 成功返回子进程pid, 失败返回-1
status : 子进程退出码, 输出型参数, 如果不关心子进程返回值可以置为NULL
注意
wait等待子进程是一个阻塞等待, 死等, 如果子进程没有退出父进程不会运行.
比如 :
#include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <sys/wait.h> int main() { printf("i am parent ---- %d\n",getpid()); pid_t pid = fork(); if(pid < 0) { perror("fork error"); } else if(pid == 0) { sleep(3); printf("i am child ---- %d\n",getpid()); exit(0); } wait(NULL); while(1) { printf("i am ----%d\n",getpid()); sleep(1); } return 0; }
程序运行结果 :
开始运行, 创建子进程, 3秒之后打印子进程信息, 子进程退出, 父进程等待子进程退出, 然后获取子进程退出返回值, 释放空间, 然后继续往下运行, 运行之前是一个阻塞等待.
2. waitpid
头文件
#include<sys/wait.h>
函数
pid_ t waitpid(pid_t pid, int *status, int options)
返回值 : 正常返回的时候返回子进程的进程ID
如果设置了选项 : WNOHANG, 如果没有发现已经退出的子进程则返回0
如果调用出错 : 返回-1, 这时error会被置成异常退出信号值
参数
pid :
-1 : 等待任意一个子进程, 与wait等效.
pid > 0 : 等待指定子进程
status :
正常退出 : 正常退出返回值
异常退出 : 异常退出信号值
options :
0 : 阻塞等待
WNOHANG : 将waitpid设置为非阻塞等待, 父进程一边干自己的事一遍等待, 如果有子进程退出则处理一下.
比如 :
#include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <wait.h> int main() { int pid = fork(); if(pid < 0) { perror("fork error"); exit(-1); } else if(pid == 0) { sleep(5); exit(0); } int statu; int ret; while((ret = waitpid(pid,&statu,WNOHANG)) == 0) { printf("打麻将\n"); sleep(1); } printf("%d--%d\n",ret,pid); while(1) { printf("--------------------\n"); sleep(1); } return 0; }
运行结果 :
运行这个程序, 创建子进程并且让他睡5秒, 父进程进行一个waitpid的非阻塞等待, 子进程睡的5秒父进程一直在打麻将, 并且判断waitpid的返回值是不是0, 如果是0, 就是没有子进程退出, 如果不是0就是有子进程退出, 5秒之后子进程退出, waitpid的返回值也就是ret变成了子进程的pid, 退出循环, 获取子进程的退出返回值status, 然后释放资源, 父进程继续运行.
获取进程退出返回值
1. wait 和 waitpid 都有一个参数 status 来获取进程的退出返回值
2. 如果传的是NULL, 那就是不关心返回值
3. status是一个4个字节的数据, 但是我们只关心它的低16位, 因为进程退出返回值是它的低16位的高8位
我们来看一下status这个参数的构成 :
获取子进程退出码 :
如果正常退出 : 低七位为0 , 退出码等于 (status >> 8) & 0xff
如果异常退出 : 低七位不为0 , 没有必要获取返回值了, 会返回异常退出信号值
判断是否正常退出 : status & 0x7f
为0 : 正常退出, 获取返回值
不为0 : 不获取返回值, 返回异常退出信号值
比如 :
#include <stdio.h> #include<stdlib.h> #include <unistd.h> #include <sys/wait.h> int main() { int pid = fork(); if(pid < 0) { perror("fork error"); exit(-1); } else if(pid == 0) { sleep(5); exit(255); } int statu; int ret; while((ret = waitpid(pid,&statu,WNOHANG)) == 0) { printf("打麻将\n"); sleep(1); } //获取进程退出返回值 if(!(statu & 0x7f)) { //正常退出 printf("%d---%d-----child exit code:%d\n",ret,pid,(statu >> 8) & 0xff); } else { printf("%d---%d----exit:%d\n",ret,pid,statu & 0x7f); } while(1) { printf("--leihoua---\n"); sleep(1); } return 0; }
我们让它正常运行 :
可以看到, 成功获取到了子进程的返回值, 而且也拿到了子进程的进程ID
我们在另一个终端中杀死子进程让它异常退出:
我们可以看到这个时候进程退出的返回值不是255而是15, 其实这是异常的信号值, 现在我们先这样理解, 后面我们具体再说信号.
但是我们每次计算是否正常退出和退出的返回值太麻烦了, 所以在库里给了一套接口
WIFEXITED(status) : 判断程序是否正常退出, 正常退出返回true.
WEXITSTATUS(status) : 获取正常退出返回值, 只有WIFEXITED返回true的时候才有用.
WIFSIGNALED(status) : 如果是否是被信号终止, 返回true.
WTERMSIG(status) : 获取异常退出信号值.
使用如下 :
#include <stdio.h> #include<stdlib.h> #include <unistd.h> #include <sys/wait.h> int main() { int pid = fork(); if(pid < 0) { perror("fork error"); exit(-1); } else if(pid == 0) { sleep(5); exit(255); } int statu; int ret; while((ret = waitpid(pid,&statu,WNOHANG)) == 0) { printf("打麻将\n"); sleep(1); } //正常退出 if(WIFEXITED(statu)) { printf("%d---%d-----child exit code:%d\n",ret,pid,WEXITSTATUS(statu)); } if(WIFSIGNALED(statu)) { printf("exit signal:%d\n",WTERMSIG(statu)); } while(1) { printf("--leihoua---\n"); sleep(1); } return 0; }
运行结果如下 :
程序替换
用fork创建子进程后执行的是和父进程相同的程序,子进程往往要调用一种exec函数 以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。
替换函数
exec函数族 :
如果调用成功直接从新的程序启动代码开始执行, 没有返回值
如果调用失败返回-1.
exec函数族中 :
有没有p的区别 : 是否自动到PATH所指定的路径下查找程序文件, 如果有就自动查找
有没有e的区别 : 是否自定义环境变量, 如果有就自定义
execl 和 execv 的区别 :
l : 参数采用列表 list
v : 参数采用数组, vector
注意 :
这些函数中只有 execve 是系统调用接口, 其它的函数都是库接口, 最终其实还是要调用execve来实现
具体替换 :
我们在这里给出几个简单的情况: 创建一个子进程去进行程序替换
#include <stdio.h>
#include <sys/wait.h>
#include <unistd.h>
int main()
{
pid_t pid = fork();
const char * envp[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL};
const char* ret[] = {"ls","-a",NULL};
if(pid < 0)
{
perror("fork error");
}
else if(pid == 0 )
{
execlp("ls","ls","-a",NULL);
execl("/bin/ls","ls","-a",NULL);
execle("/bin/ls","ls","-a",NULL,envp);
execv("/bin/ls",ret);
execvp("ls",ret);
execve("/bin/ls",ret,envp);
}
wait(NULL);
return 0;
}
每一个语句运行的结果都是 :
以上就是关于进程控制的一些问题, 感谢观看