链接在此
#include
pid_t fork(void);
返回值:
- 子进程 返回 0
- 父进程 返回 子进程 id
- 出错 返回 -1
进程调用 fork,当控制转移到内核中的 fork 代码后,内核做:
fork 之前,父进程独立执行,
fork 之后,父子两个执行流分别执行 fork 后面的代码。
注意,fork之后,谁先执行完全由调度器决定。
通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副
本。具体见下图:
fork常规用法
fork调用失败的原因
如何理解进程退出?
OS内少了一个进程,OS就要释放进程对应的内核数据结构 + 代码和数据(如果有独立的)
进程结束情况分类:
a. 执行完了,正常退出
b. 崩溃了,进程异常 (通过 信号 的方式,后面更新)
kill -9
我们说,进程是有退出码的。
比如 main 函数中的 return x; 正常执行完了
echo $?
可以 查看 最近执行的进程的 退出码
echo $?
正常终止有三种方式:
① main 函数 return。
②
void exit(int status)
函数退出
- 头文件
#include
③
void _exit(int status)
函数退出
- 头文件
#include
由此我们可以推测出的就是:
缓冲区,一定不在 OS 内,不然肯定都能刷新了,事实上并没有
这是因为用户层和 OS 之间,还有一个 C库。(后面更新)
异常退出
ctrl + C
信号终止(信号!!后面更新)
进程等待 就是 通过系统调用,获取 子进程 退出码 或者 退出信号 的方式,顺便释放内存问题。
进程等待的作用:
避免内存泄漏
获取子进程执行的结果(如果必要)
等待,实际上就是 父进程 对 子进程 的等待
pid_t wait(int *status)
头文件:#include
返回值:等待成功则返回 子进程 的 PID
pid_t waitpid(pid_t pid, int *status, int options);
头文件#include
返回值:
- > 0,success(返回的是对应子进程的 pid)
- ==-1,failed(一般不会失败,pid 传错了会导致失败)
== 0,在选项设置了 WNOHANG 的情况下,发现没有已退出的子进程可收集参数 pid:
- >0,表示等待指定的子进程
- ==-1,表示等待任一个子进程,与 wait 等效
参数 status:是一个输出型参数
- 我们会想得到,子进程的退出状态,即 信号 + 返回码
- 如何用一个整数 返回两个整数…?实际上这里的 status 是类似位图作用的。信号和返回码都不超过一定值,用一个整数位存储足够啦!(见下文)
- WIFEXITED(status):若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
- WEXITSTATUS(status):若 WIFEXITED 非零,提取子进程退出码。(查看进程的退出码)
参数 options:
- 0,默认父进程进行 阻塞等待
- WNOHANG,父进程进行 非阻塞轮询
使用举例:
int status = 0;
pid_t ret_id = waitpid(id, &status, WNOHANG);
if(ret_id < 0)
{
printf("waitpid error!\n");
exit(1);
}
else if(ret_id == 0) // 子进程还没结束
{
RunTask();
sleep(1);
continue;
}
else // 子进程结束了
{
if(WIFEXITED(status)) // 正常退出
{
printf("wait success, child exit code: %d\n", WEXITSTATUS(status));
}
else // 异常退出
{
printf("wait success, child exit signal: %d\n", status & 0x7F);
}
break;
}
status 记录 信号 和 退出码 的方式:
00000000 00000000 00000000 00000000
想要自己访问 信号 和 退出码,我们用如下方式:
// 退出码
(status>>8) & 0xFF;
// 终止信号
status & 0x7F;
所以,
父进程 如何拿到 子进程 的退出码和终止信号的呢?
实际上,进程的 PCB 中,就有这两个成员值。
struct task_struct
{
// ...
int exit_code;
int exit_signal;
task_struct* parent;
};
父进程在 wait 的时候,如果子进程没有退出,父进程只能一直调用 waitpid 进行等待,这就叫 阻塞等待。
如果我们不想 阻塞等待,即 不想在 waitpid 处卡住呢?
用 fork 创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种 exec 函数执行另一个程序。当进程调用一种 exec 函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用 exec 并不创建新进程,所以调用 exec 前后该进程的 pid 并未改变。
exec 这一系统接口,也被叫做 加载器。
简单来说:
程序替换 不产生新的进程
程序替换 是 整体替换,不能局部替换
程序替换 只会影响 调用进程,因为 进程具有独立性(写时拷贝)
接下来是 exec 系列函数,
头文件是#include
记忆:l 代表 list,把参数一个一个列表一样的传给函数。
int execl(const char *path, const char *arg, ...)
返回值:
- 成功时,发生整体替换,也不会有返回值
- 失败时,会继续调用后面的代码,此时才有返回值,返回也有意义
参数 path:
- 带路径的可执行程序
参数包:
- 在命令行怎么执行这个命令,就以空格为分割,一个一个的传给 execl 就可以了。最后以 NULL 结尾。
ps:对于该函数不用进行返回值判断,只要继续向后运行了,就一定是失败的
使用举例
execl("/bin/ls", "ls", "-a", "-l", NULL);
记忆:v 代表 vector,数组作为参数。
int execv(const char *path, char *const argv[])
返回值:
- 成功时,发生整体替换,也不会有返回值
- 失败时,会继续调用后面的代码,此时才有返回值,返回也有意义
参数 path:
- 带路径的可执行程序
参数 argv:
- 设置一个 char* const argv[] 的指针数组, 在命令行怎么执行这个命令,就以空格为分割,放入数组。最后以 NULL 结尾。
使用举例
char *const myargv[] = { "ls", "-a", "-l", NULL };
execv("/bin/ls", myargv);
记忆:不带 p 的需要指定程序路径,而带 p 的只需要指定程序名即可,系统会自动在环境变量 PATH 中查找。
int execlp(const char *file, const char *arg, ...)
返回值:
- 成功时,发生整体替换,也不会有返回值
- 失败时,会继续调用后面的代码,此时才有返回值,返回也有意义
参数 file:
- 可执行程序名,该程序必须要在环境变量 PATH 中可以被调用
参数包:
- 在命令行怎么执行这个命令,就以空格为分割,一个一个的传给 execl 就可以了。最后以 NULL 结尾。
使用举例
execlp("ls", "ls", "-a", "-l", NULL);
这里两个一样的参数,不要省略!!!即使有时候省略了也能跑,不要这样写!!
记忆: p,直接给数组名; v,以数组方式传参。
int execvp(const char *file, char *const argv[])
返回值:
- 成功时,发生整体替换,也不会有返回值
- 失败时,会继续调用后面的代码,此时才有返回值,返回也有意义
参数 file:
- 可执行程序名,该程序必须要在环境变量 PATH 中可以被调用
参数 argv:
- 设置一个 char* const argv[] 的指针数组, 在命令行怎么执行这个命令,就以空格为分割,放入数组。最后以 NULL 结尾。
使用举例
char *const myargv[] = { "ls", "-a", "-l", NULL };
execvp("ls", myargv);
记忆: p,直接给数组名; v,以数组方式传参。
int execvp(const char *path, const char *arg, ..., char *const envp[])
返回值:
- 成功时,发生整体替换,也不会有返回值
- 失败时,会继续调用后面的代码,此时才有返回值,返回也有意义
参数 path:
- 带路径的可执行程序
参数包:
- 在命令行怎么执行这个命令,就以空格为分割,一个一个的传给 execl 就可以了。最后以 NULL 结尾。
参数 envp:
- 自定义环境变量,并且是 覆盖式传入
引申另一个函数
int putenv(char *string)
头文件:#include
作用:把 参数位置 的字符串作为 环境变量,加入 PATH
使用举例
// 【系统环境变量】
extern char** environ;
/* 自定义环境变量
char *cosnt myenv[] = {
"MYENV=ThisIsValue",
NULL
};*/
// 【把自定义变量加入系统环境变量】
putenv("MYENV=ThisIsValue"); // 此时就被添加进 environ 里了
// 要求既传 系统的 又传 自定义的
execle("./otherproc", "otherproc", NULL, environ);
环境变量具有全局性,可以被子进程继承下去。 这句话如何深刻理解?
实际上,也正是 OS 调用 exec,调用的我们写的 main 函数,main 函数的参数 envp 也就是 OS 传给我们的。
不多说喽
int execvpe (const char* file, char *const argv[], char *const envp[])
int execve (const char* path, char *const argv[], char *const envp[])
这些 exec 函数,实际上,只有 execve
是系统调用,其余的 6 个都是这个调用的封装
链接在此
如果本文对你有些帮助,请给个赞或收藏,你的支持是对作者大大莫大的鼓励!!(✿◡‿◡) 欢迎评论留言~~