我们之前已经学过fork函数的应用了(从已存在进程中创建一个新进程:新进程为子进程,原进程为父进程)
接下来我们来研究一下fork函数的运行的内核是怎样的
fork()详解
fork是复制进程的函数,程序一开始就会产生一个进程(这个进程为父进程)当它执行到fork时,fork就会复制一份原来的进程,创建一个新进程(就是子进程)。他们一起向下执行父进程剩余的代码。
fork()函数在执行时:
分一块新的PCB内存块和内核数据给子进程->将父进程的数据和代码copy给子进程->将子进程的添加到父进程执行的程序列表中->fork开始返回,调度器开始调度
进程=内核的相关管理数据结构(task_struct + mm_struct + 页表)+ 代码和数据
已知fork函数的返回值是这样的:
子进程返回0
父进程返回子进程的pid
子进程的进程ID。我们可以通过fork返回的值来判断当前进程是子进程还是父进程。
进程具有独立性就在于子进程代码数据和父进程共享,但因为写时拷贝又不影响父进程
写时拷贝:一开始内核并不会把父进程的地址空间全部复制给子进程,而是让父子进程共享一段空间;只有在写时才复制地址空间,使得父子进程都拥有独立的地址空间。
即资源的复制是在只有需要写入时才会发生,因此而称之为Copy on Write(COW)。在此之前都是以读的方式去和父进程共享资源,这样,在页根本不会被写入的场景下,fork()立即执行exec(),无需对地址空间进行复制,fork()的实际开销就是复制父进程的一个页表和为子进程创建一个进程描述符,也就是说只有当进程空间中各段的内存内容发生变化时,父进程才将其内容复制一份传给子进程,大大提高了效率。
1 #include<stdlib.h>
2 #include <unistd.h>
3 #include<stdio.h>
4 int main(void)
5 {
6 pid_t pid;
7
8 printf("Before: pid is %d\n", getpid());
9 if ((pid = fork()) == -1)
10 perror("fork()"), exit(1);
11 printf("After:pid is %d, fork return %d\n", getpid(), pid);
12 sleep(1);
13 return 0;
14 }
~
fork()的常规用法
常规用法:一个父进程希望能创造一个子进程复制自己的代码和数据,使父子进程同时执行不同的代码段(父进程等待客户端的请求,生成子进程来处理请求)
一个进程要执行一个不同的程序(子进程从fork()返回后,调用exec函数)
进程调用一种 exec 函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行(当前进程剩下的程序代码便不再执行)。调用 exec 并不创建新进程,所以调用 exec 前后该进程的id并未改变。
fork调用失败原因
系统中有太多的进程:实际用户的进程数超过了限制导致调用失败
进程终止
进程终止是指释放曾经的代码和数据所占据的空间,也是在释放内核数据结构(task_struct,当进程状态是Z就要释放对应PCB)
终止的三种情况
来看两段代码
#include<stdio.h>
#include<unistd.h>
int main()
{
printf("hello world!\n");
return 0;
}
#include<stdio.h>
#include<unistd.h>
int main()
{
printf("hello world!\n");
return 100;
}
可以看到他们只有返回值不同
($) 是 bash 脚本中的通用特殊字符,用于变量和命令替换、表示特殊变量以及用作转义字符。
Shell特殊变量讲解:$n、$#、$*、$@、$?、$$各自代表的意思及用法-优快云博客
$在这里我们使用他的一个用法:$?
?:父进程bash获取到的最近一个子进程的退出码(0就是成功,非0就是失败)
来我们使用一下:
如此我们可以看出退出码的意义:告诉关心方(父进程在这里就是关心方)任务完成的怎么样
因为成功的退出码就是0,而!0有很多,所以不同!0值一方面表示失败,一方面还表示失败的原因(我们可以规定不同的返回值对应出不同的错误)
像这样:
#include<stdio.h>
#include<unistd.h>
#include<string.h>
int main()
{
in