TheRiver | blog

You have reached the world's edge, none but devils play past here

0%

Linux-fork和clone

参考

Linux fork那些隐藏的开销

Unix/Linux fork后传-clone

Unix/Linux fork/exec的前世今生

朴素的UNIX之-进程/线程模型

线程库中的创建线程和退出线程

Linux中fork,vfork和clone详解(区别与联系)


正文

fork()

man-pages

   #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.png

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那些隐藏的开销

死锁

具体参考Linux fork那些隐藏的开销

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

pthread_mutex_t mutex;

void *mmap_unmap(void *arg)
{
while (1) {
pthread_mutex_lock(&mutex);
sleep (4);
pthread_mutex_unlock(&mutex);
}
}
int main(int argc, char *argv[])
{
pthread_t tid;

pthread_mutex_init(&mutex,NULL);
pthread_create(&tid, NULL, mmap_unmap, NULL);

sleep(1);
if (fork() == 0) {
pthread_mutex_lock(&mutex);
pthread_mutex_unlock(&mutex);
printf("未死锁!\n\n");
}
sleep(1000);
return 0;
}


//父进程        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

man page

#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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

pthread_mutex_t mutex;

void *mmap_unmap(void *arg)
{
printf("hello!\n");
}
int main(int argc, char *argv[])
{
pthread_t tid;

pthread_create(&tid, NULL, mmap_unmap, NULL);
sleep(10);

return 0;
}


关键的系统调用:

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就是常见的线程的实现(内核级线程)

图片来自:Unix/Linux fork后传-clone

TGID_lit.jpg

linux内核级线程

内核级线程:

内核级线程又称为内核支持的线程

  • 内核管理所有线程管理,并向应用程序提供API接口
  • 内核维护进程和线程的上下文
  • 线程的切换需要内核支持
  • 以线程为基础进行调度
  • 例子:Windows

图片来源: https://blog.csdn.net/gatieme/article/details/51892437

20200301_内核级线程.jpeg

linux下,进程线程都是task_struct,通过CLONE_THREAD,新的进程和调用者进程的TGID是一样的,都是线程组头线程的PID.从Linux 2.4开始,getpid(2)会返回调用者的TGID。所以看到的表象就是线程组内的进程都是一个进程ID,通过CLONE_VM,这些线程组内的进程还可以公用虚拟空间,没有地址隔离,和进程的概念完全区别了.最后,exit只推出当前的task_struct,而exit_group退出线程组内所有线程的task_struct.

结语

本文参考了大量文章,有的地方也还不是特别清楚,没有亲自去验证,去看底层实现.只能站在别人的肩膀上瞧瞧了.


ending

76922689_p0_master1200.jpg

----------- ending -----------