1. 进程拥有资源 mm,fs,files,signal…
fork 创建一个新进程,也需要创建 task_struct 所有资源;
实际上创建一个新进程之初,子进程完全拷贝父进程资源,如下图示:
比如 fs 结构体:
子进程会拷贝一份 fs_struct,
*p2_fs = *p1_fs;
pwd 路径和 root 路径与父进程相同,子进程修改当前路径,就会修改其 p2_fs->pwd 值;父进程修改当前路径,修改 p1_fs->pwd;
其他资源大体与 fs 类似,最复杂的是 mm 拷贝,需借助 MMU 来完成拷贝;即写时拷贝技术:
2. 写时拷贝技术:
#include <sched.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int data = 10;
int child_process()
{printf(“Child process %d, data %d\n”,getpid(),data);
data = 20;
printf(“Child process %d, data %d\n”,getpid(),data);
_exit(0);
int main(int argc, char* argv[])
int pid;
pid = fork();
if(pid == 0) {child_process();
else{sleep(1);
printf(“Parent process %d, data %d\n”,getpid(), data);
exit(0);
return 0;
第一阶段:只有一个进程 P1,数据段可读可写:
第二阶段,调用 fork 之后创建子进程 P2,P2 完全拷贝一份 P1 的 mm_struct,其指针指向相同地址,即 P1/P2 虚拟地址,物理地址完全相同,但该内存的页表地址变为只读;
第三阶段:当 P2 改写 data 时,子进程改写只读内存,会引起内存缺页中断,在 ISR 中申请一片新内存,通常是 4K,把 P1 进程的 data 拷贝到这 4K 新内存。再修改页表,改变虚实地址转换关系,使物理地址指向新申请的 4K,这样子进程 P2 就得到新的 4K 内存,并修改权限为可读写,然后从中断返回到 P2 进程写 data 才会成功。整个过程虚拟地址不变,对应用程序员来说,感觉不到地址变化。
谁先写,谁申请新物理内存;
Data=20;
这句代码经过了赋值无写权限,引起缺页中断,申请内存,修改页表,拷贝数据…回到 data=20 再次赋值,所以整个执行时间会很长。
这就是 linux 中的写时拷贝技术(copy on write), 谁先写谁申请新内存,没有优先顺序;
cow 依赖硬件 MMU 实现,没有 MMU 的系统就没法实现 cow,也就不支持 fork 函数, 只有 vfork;
3. vfork 的 mm 指针直接指向父进程 mm;
除了 mm 共享,其他资源全都拷贝一份,而 fork 是所有资源都对拷一份,对比如下图
不同点:vfork 会阻塞:
vfork 后,父进程会阻塞,直到子进程调用 exit()或 exec,否则父进程一直阻塞不执行;
上面代码改用 vfork,打印输出 10,20,20
4. 线程
clone 函数创建一个新进程,不执行任何拷贝,所有资源都等同 vfork 中的 mm 共享,task_struct 里只有指针指向父进程 task_struct;
也就是子进程与父进程完全 共享资源 ,但是又可以被 独立调度,实际上这就是 linux 中的线程本质;
pthread_create()函数就是调用 clone()函数 (带有 clone_flags) 创建新 task_struct,其内部 mm,fs 等指针全都指向父进程 task_struct;
Linux 中创建进程 (fork,vfork) 和线程(pthread_create),在内核都是调用 do_fork()-->clone(),参数 clone_flags 标记表明哪些资源是需要克隆的,创建线程时,所有资源都不克隆;
从调度的角度理解线程,从资源角度来理解进程,内核里只要是 task_struct,就可以被调度;linux 中的线程又叫轻量级进程 lwp;
ret = pthread_create(&tid1, NULL, thread_fun, NULL);
if (ret == -1) {perror("cannot create new thread");
return -1;
strace ./a.out
5. 人妖
如上述,资源全部共享是线程,不共享是进程;那假如修改 clone 函数中的 clone_flags,使共享其中部分资源,如下图示:
这时候创建的既不是进程也不是线程,妖有了仁慈的心, 就不再是妖, 是人妖;
Linux 是可以调用 clone 创建人妖的,不过没实际必要~
6.PID
Linux 的每个线程都会创建 task_struct,会有个独立的 PID;
POSIX 标准规定,在多线程中调用 getpid()应该获得相同的 PID;
为兼容 POSIX 标准,linux 增加了一层 TGID,
调用 getpid()实际上是去 TGID 层获取 PID,TGID 中 PID 均相同,保留了线程在内核中不同的 PID,如下图所示:
top 命令看到的是进程 TGID,所有线程相同;
top –H 命令是从线程视角,此时的 PID 是 task_struct 中实际的 PID;
7. 进程死亡:
7.1 子进程先死亡,父进程去清理,所谓白发人送黑发人,不清理则变成僵尸进程;
7.2 若父进程先死,子进程变成孤儿,一般托付给 init,新版 linux3.4 引入 subreaper,可以托付给中间进程 subreaper。
父进程先死亡后,子进程沿 tree 向上找最近的 subreaper 挂靠,找不到 subreaper,就挂在 init。
/* Become reaper of our children */
if (prctl(PR_SET_CHILD_SUBREAPER, 1) < 0) {log_warning("Failed to make us a subreaper: %m");
if (errno == EINVAL) {log_info("Perhaps the kernel version is too old (< 3.4?)");
PR_SET_CHILD_SUBREAPER 设置为非零值,当前进程就会变成 subreaper,会像 1 号进程那样收养孤儿进程;
8. 睡眠
当进程需要等待硬件 I / O 资源的时候,可以设置为睡眠状态,一般驱动做成浅度睡眠,硬盘等资源会置入深度睡眠(不会被信号唤醒);
睡眠是把 task_struct 挂在 wait queue 上,比如多个进程都在等待串口,当串口可用时,唤醒等待队列上所有进程;
以下为《linux 设备驱动开发详解》中案例注释
注:上图有个错误,while 循环中,应该为
“若非阻塞,直接退出”;
”进程阻塞,将进程设置睡眠状态“
当读取 fifo 为空即 dev->current_len== 0 时,将进程加入等待队列睡眠,schedule()让出 CPU, fifo 中写入数据时将等待队列唤醒,此函数中 schedule()处继续执行;唤醒动作在 write 函数中执行;
唤醒后检查唤醒原因,若为 IO 唤醒,正常读取数据;若为信号唤醒,直接退出;
9. 0 进程
0 进程是唯一没通过 fork()创建的进程,是系统中所有其它用户进程的祖先进程,其创建 1 号进程 (init 进程) 后,退化为 idle 进程,也叫 swapper 进程;
top 命令中的 id 时间即为 idle 进程运行时间;
idle 进程:优先级是最低的,当系统中没有任何进程运行时,即执行 idle 进程,idle 将 CPU 置入低功耗模式,有任何其他进程被唤醒,idle 即让出 CPU;
idle 进程的设计,实际上是将“跑”与“不跑”的问题,统一为“跑”的问题。极巧妙的简化了系统设计,降低进程之间的耦合度。(将检查系统是否空闲,设置 CPU 低功耗模式的功能放在 idle 实现,其他进程都不用关心 CPU 工作模式)