接续上篇博客 “详解进程控制 ( fork函数 | 写时拷贝 | 进程退出 | 进程等待 )”
再谈进程退出
- 进程退出会变成僵尸状态,将自己的推出结果写入task_struct
- wait与waitpid是一个系统调用,操作系统有资格也有能力去读取子进程的task_struct
- wait与waitpid就是从退出子进程的task_struct中获取的
感性认识阻塞与非阻塞:
假设张三喊李四去吃饭,张三给李四打电话,但是李四说他还有事大概需要等30分钟,这时候张三:
1.张三不挂电话,一直等到李四说我好了然后去吃饭 ----- 阻塞
2.张三把电话挂了,然后去做自己的事情边做边等,过个几分钟给李四打电话问他好了没,过个几分钟又打电话……-----每一次打电话都是一次非阻塞等待(多次非阻塞等待\轮询)
打电话对应的是系统调用wait\waitpid, 张三(父进程)、李四(子进程)
轮询访问父进程代码:
//父进程
int status = 0;
while(1)
{
pid_t ret = waitpid(id, &status, WNOHANG); //WNOHANG: 非阻塞-> 子进程没有退出, 父进程检测时候,立即返回
if(ret == 0)
{
// waitpid调用成功 && 子进程没退出
printf("wait done, but child is running...., parent running other things\n");
}
else if(ret > 0)
{
// 1.waitpid调用成功 && 子进程退出了
printf("wait success, exit code: %d, sig: %d\n", (status>>8)&0xFF, status & 0x7F);
break;
}
else
{
// waitpid调用失败,比如我的waitpid的函数参数id写错
printf("waitpid call failed\n");
break;
}
sleep(1);
}
运行结果:
非阻塞状态有什么好处?不会占用父进程的所有精力,可以在轮询期间干干别的事情。
进程程序替换
创建子进程目的:
- 想让子进程执行父进程代码的一部分(执行父进程对应的磁盘代码中的一部分)
- 想让子进程执行一个全新的程序(让子进程想办法,加载磁盘上指定的程序,执行新程序的代码和数据)
引入
int execl(const char *path, const char *arg, ...);
//将指定的程序加载到内存中,让指定进程进行执行
//path是用来找到这个程序,如何执行就是后面的arg传入的选项
比如:
1 #include <stdio.h>
2 #include <unistd.h>
3 int main()
4 {
5 printf("process is running...\n");
6
7 execl("/usr/bin/ls","ls",NULL);
8
9 printf("process is done...\n");
10 return 0;
11 }
运行结果:
我将增加几个执行选项,并加上颜色显示:
execl("/usr/bin/ls","ls","--color=auto","-a","-l",NULL);
上述过程就是程序替换,我们只要能找到这个程序,然后添加它的运行选项就可以执行。
程序替换原理
我们看之前的代码打印出来的结果:
程序执行图:
为什么后一个printf没有执行?
printf也是代码,且在execl函数之后,execl函数执行完毕的时候,代码已经全部被复制,开始执行新的程序的代码了,所以printf就无法执行。
execl是一个函数,而一个函数的调用就有可能成功有可能失败,调用失败就是没有替换成功,也就是没替换,所以会继续执行原代码:
exec*没有成功返回值,因为成功了就与接下来的原代码无关,判断没有意义。execl只要返回了,一定是错误了。
父进程创建子进程,子进程的替换会影响父进程吗?
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <stdlib.h>
4 #include <assert.h>
5 #include <sys/types.h>
6 #include <sys/wait.h>
7
8 int main()
9 {
10 printf("process is running...\n");
11 pid_t id = fork();
12 assert(id != -1);
13 if(id==0)
14 {
15 sleep(1);
16 execl("/usr/bin/ls","ls","-a",NULL);
17 exit(1);
18 }
19 int status=0;
20 pid_t ret = waitpid(id,&status,0);
21 if(ret>0)
22 {
23 printf("wait sucess : exit code:%d, signal:%d\n",(status>>8)&0xFF,status & 0x7F);
24 }
25 return 0;
26 }
进程具有独立性,子进程成功调用exec*会发生写时拷贝,在物理内存里面重新加载代码与数据,然后更改子进程的页表映射,此过程不会影响父进程。
虚拟地址空间+页表 : 保证了进程独立性,一旦有执行流想替换代码或数据,就会发生写时拷贝
有哪些替换函数
使用man指令查看execl函数
这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。如果调用出错则返回-1,所以exec函数只有出错的返回值而没有成功的返回值。
这些函数开头都是exec,而后面跟的字符可以代表不同含义:
- l(list) : 表示参数采用列表
- v(vector) : 参数用数组
- p(path) : 有p自动搜索环境变量PATH
- e(env) : 表示自己维护环境变量
execl:
int execl(const char *path, const char *arg, ...);
l:list:将参数一个一个的传入exec*
替换自己写的可执行程序:
execl("./myexe","myexe",NULL);
程序替换,可以使用程序替换,调用任何后端语言对应的可执行–程序
execlp:
int execlp(const char *file, const char *arg, ...);
//带p无需写全路径,可以直接使用环境变量PATH
execlp("ls", "ls", "-a", "-l", "--color=auto", NULL);
这里有两个ls, 重复吗?不重复,一个是告诉系统我要执行谁?一个是告诉系统,我想怎么执行
p:path:如何找到程序的功能,带p字符的函数,不用告诉我程序的路径,你只要告诉我是谁,我会自动在环境变量PATH,进行可执行程序的查找!
execv:
int execv(const char *path, char *const argv[]);
char *const argv_[] = {
"ls",
"-a",
"-l",
"--color=auto",
NULL
};
execv("/usr/bin/ls", argv_);
v:vector:可以将所有的执行参数,放入数组中,统一传递,而不用进行使用可变参数方案
execvp:
int execvp(const char *file, char *const argv[]);
//将 v 与 p 的特性结合起来
char *const argv_[] = {
"ls",
"-a",
"-l",
"--color=auto",
NULL
};
execvp("ls", argv_);
int main(int argc, char *argv[])
// ./exec ls -a -l -> "./exec" "ls" "-a" "-l"
execvp(argv[1], &argv[1]);
运行结果:
execle:
int execle(const char *path, const char *arg, ...,char *const envp[]);
//需要自己组装环境变量
char *const envp[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL};
execle("ps", "ps", "-ef", NULL, envp)
自定义环境变量举个例子:
为了即使用全局环境变量又添加自己的环境变量,我们使用putenv来增加以一个环境变量:
先加载execle后执行main:
execve:
我们使用man手册打开它,可以知道它在2号手册里,代表他是系统调用
事实上,只有execve是真正的系统调用,其它五个函数最终都调用 execve,所以execve在man手册 第2节,其它函数在man手册第3节
程序替换中execve为系统调用,其它都是封装,为了让我们有更多的选择性
如有错误或者不清楚的地方欢迎私信或者评论指出🚀🚀