创建进程 -- fork()的原理
首先要问几个问题:
1、为什么给子进程返回0,给父进程返回子进程的 pid ?
2、如何理解 fork() 有两个返回值?
接下来通过了解 fork() 原理解答:
回答第一个问题,因为父进程需要区分子进程,而子进程只有一个父亲且不管理父进程,所以不需要返回。
而第二个问题需要从 fork() 内部分析,首先 fork() 是一个函数,返回值是 pid_t 类型,当进入 fork() 时,会先进行拷贝父进程,拷贝父进程的 PCB ,拷贝 task_struct、mm_struct、页表、文件等等。
在子进程拷贝和 return 返回值的区间,会进行分流,执行两个进程,就相当于一个 fork() 分成了两个 return,所以才会有两个返回值。所以说两个返回值是两个分流再执行 pid 导致的,而不是一个函数返回两个值的意思。
内核:分配新的内存块和内核数据结构给子进程,拷贝父进程的内容,添加子进程到系统的进程列表中,fork 返回。
为何要写时拷贝?
1、进程具有独立性。2、子进程不一定会使用父进程所有的数据,做到按需分配(你都不一定该我给你分什么),延时分配(你立刻调用吗?轮到再给你)
综上可以看出,物理内存空间的分配由 OS 管理(内存管理),不允许进程调度 —— 解耦。
代码会写时拷贝吗?
不是完全不会,进程替换就是。
进程终止
查看退出码:
echo $? // 查看的是最近一次,包括指令
退出码有 0 和 非0:0 代表成功只有一种情况,非0 代表失败,有很多种情况。
查看错误码信息:
strerror(n) // n 代表退出码
每种退出码都有对应的字符串含义,任务失败的原因。
exit(n) // n 是进程退出码
return 是返回 main 函数,不是终止,而有了 exit(),就不仅限于 main 函数了,在任何地方都可以终止。
exit() 和 _exit() 的功能一样:
exit() 在进程退出时,会刷新缓冲区;
_exit() 比较暴力,不会刷新缓冲区;
exit() 会释放进程曾经占用的资源,而 _exit() 直接终止。
问:进程异常退出了,退出码还有意义吗?
答:没有意义!,因为异常行为没走到 return 就退出,所以 return 的值没有意义。
进程等待
进程等待:父进程回收子进程资源,获取子进程退出信息
进程等待的方法:
1、wait 方法:
#include <sys/wait.h>
#include <sys/types.h>
pid_t wait(int *status)
正常返回 id,错误返回 -1。不关心 status 可以加 NULL。
阻塞等待:
在子进程运行期间,父进程 wait 时,在等待子进程退出。
2、waitpid 方法
waitpid 有阻塞等待和非阻塞等待,用 options 代表,0代表阻塞等待。
pid_t waitpid(pid_t pid, int *status, int options)
waitpid 可以等待指定的一个进程,wait 等待任意一个进程。
问:进程等待成功是否意味着子进程运行成功?
答:不是的,所以用 status 获取信息。
该图是 status 所代表的含义,status 用 4 个字节,也就是 32 个比特位中的低 16 位存储 status。
次 8 位代表进程退出码,低 7 位,代表进程退出时的退出信号,高 16 位不关心。正常组织用次 8,异常终止用低 7。如何接收呢?
int status = 0;
//pid_t i = wait(&status);
pid_t ret = waitpid(id, &status, 0);
if (ret >= 0){
printf("parent running! pid : %d\n",getpid());
printf("child exit code : %d\n", (status >> 8)&0xFF);
printf("child get signal : %d\n", status & 0x7F);
}
如果一个进程被异常中止,那么它的退出码是没有意义的,怎么看呢?如果有 signal 值不是 0,那么 code 就一定没意义。
为了方便,可以用另外一种方法:
if (WIFEXITED(status)){
printf("%d\n", WEXITSTATUS(status));
}
WIFEXITED(status):检测进程是否异常中止,本质是检查信号低 7 位比特位是否为 0。
WEXITSTATUS(status):判断后若非0,获取进程退出码。
非阻塞等待:WNOHANG
pid_t ret = waitpid(id, &status, WNOHANG);
若 pid 指定的子进程没有结束,则 waitpid() 返回 0,不等待,若正常结束,返回子进程的 pid。
可以用 while 进行非阻塞接口的轮循方案:if 返回值等于 0:说明子进程还在运行状态,等待成功;else if 大于 0:返回 pid 等待完成; else 小于 0:等待失败。
进程替换
进程替换:让子进程执行新的进程
exec 的 6 类函数:
#include <unistd.h>
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ...,char *const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execve(const char *path, char *const argv[], char *const envp[]);
const char *path 是命令行参数地址,const char *arg 是执行对象。
例如: execl("/usr/bin/ls", "ls", "-a", "-i", "-l", NULL)
进程替换没有进行进程创建,但是调用后不再返回,也就说,exec 后的代码以及被替换。如果失败了,就不会被替换,程序后序不会受到影响。路径、传参会导致代码替换失败。
当 fork() 创建子进程后,子进程想要替换成新的程序,用 exec 函数,其实是完全替换掉原来的数据和代码,在替换之前会发生写时拷贝!!!
exec 系列的函数,根本不需要判断返回值,返回就是失败了。
exec开头:
“ l ” :list,表示参数采用列表,一个一个传;
execl("/usr/bin/ls", "ls", "-a", "-i", "-o", NULL);
“ v ”:vector,表示给个数组,不要一个一个给;
char* myarg[] = {"ls", "-a", "-i", "-o", NULL};
execv("/usr/bin/ls", myarg);
“ p ” :path,告诉执行标准就行,不要带路径,会字节到环境变量列表中找;
execlp("ls", "ls", "-a", "-i", "-o", NULL);
“ e ” :自己组装环境变量,自己的环境变量是覆盖式,有系统无你,有你无系统;
char* myenvp[] = {"MYVAL=YOU CAN SEE ME"};
execle("/usr/bin/ls", "ls", myenvp);
前五个函数的底层都是调用 execve() 函数。
学会了这几个函数,就可以自己写一个小的 shell 外壳了。
在系统互动及用户登录时,某些软件会自动调用 bash 程序变成进程。