参考
Linux中fork,vfork和clone详解(区别与联系)
正文
fork()
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
fork() creates a new process by duplicating(复制) the calling process. The new process is referred to as the child process. The calling process is referred to as the parent process.
图片来源: 链接
fork的开销
- 写时复制导致的内存开销
- 页目录和页表
- vm_area_struct对象
- 死锁问题
写时复制导致的内存开销
fork后exec前,父进程修改常驻内存中的值,因为写时复制的原因,子进程会讲这些在内存中建立一个新的副本.(如果父进程不修改这些值,写时复制下,就没有这样的开销).可以使用vfork避免(子进程和父进程公用地址空间,子进程先于父进程执行,直到子进程exit/exec后父进程执行)
页目录和页表
fork后,在子进程exec执行前(刷新新的页目录,页表),子进程会把父进程的页目录与页表进行复制,占用了内存,浪费了时间。具体参考Linux fork那些隐藏的开销
vm_area_struct对象
fork后子进程也会复制父进程的vm_area_struct对象,如果父进程malloc/mmap很多,并且vm_area_struct对象都已经实际生成,则又造成大量的浪费。(exec前)具体参考Linux fork那些隐藏的开销
死锁
1 |
|
//父进程 103420
//父进程线程 103421
//子进程 103423
a.out(103420)─┬─a.out(103423)
└─{a.out}(103421)
Thread 2 (Thread 0x7f3e22f18700 (LWP 103421)):
#0 0x00007f3e22fd648d in nanosleep () from /lib64/libc.so.6
#1 0x00007f3e22fd6324 in sleep () from /lib64/libc.so.6
#2 0x0000000000400820 in mmap_unmap(void*) ()
#3 0x00007f3e23b00df5 in start_thread () from /lib64/libpthread.so.0
#4 0x00007f3e2300f1ad in clone () from /lib64/libc.so.6
Thread 1 (Thread 0x7f3e23f1c740 (LWP 103420)):
#0 0x00007f3e22fd648d in nanosleep () from /lib64/libc.so.6
#1 0x00007f3e22fd6324 in sleep () from /lib64/libc.so.6
#2 0x00000000004008a5 in main ()
//103423
#0 0x00007f3e23b06f7d in __lll_lock_wait () from /lib64/libpthread.so.0
#1 0x00007f3e23b02d32 in _L_lock_791 () from /lib64/libpthread.so.0
#2 0x00007f3e23b02c38 in pthread_mutex_lock () from /lib64/libpthread.so.0
#3 0x0000000000400887 in main ()
子进程在持有锁的情况下,再次lock,导致死锁
(gdb) p mutex
$1 = 2
(gdb) call pthread_mutex_unlock(&mutex)
$2 = 0
(gdb) p mutex
$3 = 0
clone
#define _GNU_SOURCE
#include <sched.h>
int clone(int (*fn)(void *), void *stack, int flags, void *arg, ...
/* pid_t *parent_tid, void *tls, pid_t *child_tid */ );
/* For the prototype of the raw clone() system call, see NOTES */
flags mask
mask | description |
---|---|
CLONE_FILES | 共享文件描述符 |
CLONE_PID | 父子进程pid相同 |
CLONE_PTRACE | 若父进程被trace,子进程也被trace |
CLONE_THREAD | 支持POSIX线程标准,子进程与父进程共享相同的线程群(内核级线程) |
CLONE_SIGHAND | 共享信号表 |
CLONE_VFORK | 子进程先于父进程执行,类似vfrok |
CLONE_FILES | 公用虚拟空间,mm_struct引用加一,不需要再复制一份vm_are_struct |
与fork(2)相比,这些系统调用可以更精确地控制在 调用进程和子进程之间共享哪些执行上下文。例如,使用这些系统调用,调用者可以控制两个进程是否共享虚拟地址空间,文件描述符表和信号处理程序表。这些系统调用还允许将新的子进程放置在单独的namespaces(7)中。
CLONE_SIGHAND
这里拷贝自:线程库中的创建线程和退出线程
如果CLONE_SIGHAND被设置,调用进程和子进程共享信号处理器表。如果某个进程修改了信号的行为(通过sigaction(2)),会影响到另外的一个进程。但调用进程和子进程仍然拥有独立的信号掩码和信号处理集。所以他们可以使用sigprocmask(2)来阻塞和释放信号而不影响另外一个。
如果CLONE_SIGHAND没有被设置,子进程只会继承调用进程的信号处理函数副本,两个进程修改信号的行为不会影响另一个进程。
从Linux2.6.0开始,如果CLONE_SIGHAND被设置了,那么CLONE_VM也需要被设置。
CLONE_VM
这里拷贝自:线程库中的创建线程和退出线程
如果CLONE_VM被设置了,那么调用进程和子进程共享内存空间。特别的,两个进程的内存写操作对另一个进程是透明的。更进一步,任何内存映射和释放映射都会影响另一个进程。
如果CLONE_VM没有被设置,那么子进程和调用进程运行在不同的内存空间中,即不共享内存空间。对进程中的内存操作不会影响另外一个进程。
CLONE_THREAD
这里拷贝自:线程库中的创建线程和退出线程
CLONE_THREAD如果被设置,那么这个clone出来的子进程会和调用clone的进程进一个线程组,这里的”线程”指的是线程组里的进程
线程组是Linux 2.4版本加入的,用来支持POSIX thread的一个概念,即一个线程组共享一个PID。在内部,这个共享PID被叫做线程组ID,即TGID。从Linux 2.4开始,getpid(2)会返回调用者的TGID。
如果clone(2)没有指定CLONE_THREAD,那么新线程将会自己一个作为一个新的线程组,其中,TGID = TID。这个线程是新线程的头儿。
带CLONE_THREAD新建的线程和clone(2)的调用者有着一样的父进程。表现为getppid(2)返回相同的值。当一个CLONE_THREAD线程终止时,创建它的线程不会发送SIGCHLD信号以及其他终止信号;终止线程的状态也不会被wait(2)捕捉。(线程称为分离的)。只有当线程组中的线程都终止时,父进程会收到SIGCHLD信号。
如果线程组中的线程执行了execve(2),那么除了线程头外的所有线程会终止,并且这个execve执行的新程序会在线程组头执行。
如果线程组中的一个线程使用fork(2),那么线程组中的所有线程能够wait(2)这个子进程。
从Linux2.5.35开始,如果CLONE_THREAD被设置,那么CLONE_SIGHAND也需要被设置。需要注意的是,从Linux 2.6.0开始,CLONE_VM也需要跟着CLONE_SIGHAND一起被设置。
使用kill(2)来向线程组所有线程发送信号,使用tgkill()来向特定的线程发送信号。
信号的处理和行为是进程范围的,比如一个未处理的信号发送给一个线程,那么这个信号会影响到线程组中的所有线程。
代码分析
1 |
|
关键的系统调用:
clone(child_stack=0x7fccd9a28fb0, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID, parent_tidptr=0x7fccd9a299d0, tls=0x7fccd9a29700, child_tidptr=0x7fccd9a299d0) = 109622
[pid 109622] exit(0)
exit_group(0) = ?
CLONE_THREAD会将新的”进程”和调用进程加入一个相同的线程组,TGID.线程组的头线程推出调用exit_group,其他线程推出调用exit.再结合CLONE_VM就是常见的线程的实现(内核级线程)
linux内核级线程
内核级线程:
内核级线程又称为内核支持的线程
- 内核管理所有线程管理,并向应用程序提供API接口
- 内核维护进程和线程的上下文
- 线程的切换需要内核支持
- 以线程为基础进行调度
- 例子:Windows
图片来源: https://blog.csdn.net/gatieme/article/details/51892437
linux下,进程线程都是task_struct,通过CLONE_THREAD,新的进程和调用者进程的TGID是一样的,都是线程组头线程的PID.从Linux 2.4开始,getpid(2)会返回调用者的TGID。所以看到的表象就是线程组内的进程都是一个进程ID,通过CLONE_VM,这些线程组内的进程还可以公用虚拟空间,没有地址隔离,和进程的概念完全区别了.最后,exit只推出当前的task_struct,而exit_group退出线程组内所有线程的task_struct.
结语
本文参考了大量文章,有的地方也还不是特别清楚,没有亲自去验证,去看底层实现.只能站在别人的肩膀上瞧瞧了.