目录
进程控制
进程创建
fork()
vfork()
fork 与 vfork的区别
进程终止
进程终止的场景 :
进程终止有三种方式 :
进程退出返回值
进程等待
为什么要进行进程等待?
进程等待的方法
获取进程退出返回值
程序替换
替换函数
具体替换 :
进程控制大概分为 : 进程创建, 进程退出, 进程等待, 程序替换
进程创建方式 : fork(), vfork().
fork()
1. 头文件 : #include
2. 返回值 : pid_t fork(void)
创建成功返回0, 失败返回-1, 父进程返回子进程的pid
创建子进程 :
#include
#include #include #include 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
2. 返回值 : 成功返回0
我们先来看一段代码:
#include
#include #include #include 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
int main() { printf("nihao\n"); return 0; } 调用return其实相当于调用exit(), 因为函数会将main的返回值当做exit的参数
2. exit()
头文件 : #include
#include
#include int main() { printf("nihao\n"); exit(0); } 3. _exit()
头文件 : #include
#include
#include int main() { printf("nihao\n"); _exit(0); }
我们通过 : echo $? 查看进程正常退出的返回值
比如:
#include
#include int main() { printf("nihao\n"); exit(255); } 获取返回值 :
这里我们还有一个概念叫做 : 错误编号
系统调用完毕都会重置进程中errno这么一个全局变量,这个全局变量中存储的就是当次调用的系统调用接口错误编号,当系统调用接口出错, 用户就可以通过这个errno获取系统调用的错误原因
比如 :
#include
#include #include int main() { //先睡十秒 sleep(10); printf("nihao\n"); exit(255); } 我们Ctrl + c 结束终止进程, 然后获取退出返回值 :
我们可以看到, 返回值并不是255, 而是130, 其实当程序异常退出的时候回返回一个未知数.
1. 我们之前在进程概念的章节说过, 如果不进行进程等待, 子进程退出父进程不知道, 会造成僵尸进程, 进而会内存泄漏
2. 僵尸进程无法杀死, kill -9 也不行
3. 父进程需要知道子进程的任务完成的如何, 以及结果是否正确, 然后回收子进程的资源, 回去子进程的退出信息
wait 和 waitpid
具体的有阻塞和非阻塞两种 :
阻塞 : 为了完成功能发起调用, 如果当前不具备完成条件, 则一直等待, 直到完成后返回
非阻塞 : 为了完成功能发起调用, 如果当前不具备完成条件, 立即报错返回
1. wait
头文件 :
#include
#include
函数 :
pid_t wait(int *status);
返回值 : 成功返回子进程pid, 失败返回-1
status : 子进程退出码, 输出型参数, 如果不关心子进程返回值可以置为NULL
注意
wait等待子进程是一个阻塞等待, 死等, 如果子进程没有退出父进程不会运行.
比如 :
#include
#include #include #include 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
函数
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
#include #include #include 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
#include #include #include 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
#include #include #include 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
#include
#include
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;
}
每一个语句运行的结果都是 :
以上就是关于进程控制的一些问题, 感谢观看