在多线程程序里面fork

来源:互联网 发布:lock java 详解 编辑:程序博客网 时间:2024/05/14 14:06
在多线程程序里面fork?没错,尽管这是一种很奇怪、以至于几乎不会有人使用的玩法,并且存在死锁等不确定因素。不过讨论讨论倒也挺有意思。

进程A,创建了3个线程。
$ ls /proc/A/task/ | wc -l
3

这时候,A调用fork,创建了进程B。那么B有几个线程呢?
$ ls /proc/B/task/ | wc -l
1
从《linux线程浅析》可以看出,linux所谓的“进程”和“线程”,本质上都是同样的“执行体”。A进程是一个执行体,而fork是对A的复制,所以它调用fork创建出来的B进程也只是一个执行体。

再来看看内存。
$ cat /proc/A/maps
...
00501000-00522000 rwxp 00501000 00:00 0 
40000000-40001000 ---p 40000000 00:00 0 
40001000-40a01000 rw-p 40001000 00:00 0 
40a01000-40a02000 ---p 40a01000 00:00 0 
40a02000-41402000 rw-p 40a02000 00:00 0 
41402000-41403000 ---p 41402000 00:00 0 
41403000-41e03000 rw-p 41403000 00:00 0 
...
我们可以看到进程A的内存分配情况。注意,里面包含了3个线程的栈空间。

fork得到的进程B呢?
$ cat /proc/B/maps
...
00501000-00522000 rwxp 00501000 00:00 0 
40000000-40001000 ---p 40000000 00:00 0 
40001000-40a01000 rw-p 40001000 00:00 0 
40a01000-40a02000 ---p 40a01000 00:00 0 
40a02000-41402000 rw-p 40a02000 00:00 0 
41402000-41403000 ---p 41402000 00:00 0 
41403000-41e03000 rw-p 41403000 00:00 0 
...
跟A一模一样,尽管线程的“执行体”没有被复制,但是栈空间却都被复制了。
因为fork会对进程A的资源进行完全的复制,而A上面的3个线程的栈空间都是在A的内存空间上的,所以栈都被复制了。光有栈而没有执行体,那么,在进程B上面这些栈空间是不是就成了垃圾了呢?的确应该是这样的。

那么,我们在进程B上面也创建3个线程看看呢?这样,B上面是不是将存在6个线程栈呢?
$ cat /proc/B/maps
...
00501000-00522000 rwxp 00501000 00:00 0 
40000000-40001000 ---p 40000000 00:00 0 
40001000-40a01000 rw-p 40001000 00:00 0 
40a01000-40a02000 ---p 40a01000 00:00 0 
40a02000-41402000 rw-p 40a02000 00:00 0 
41402000-41403000 ---p 41402000 00:00 0 
41403000-41e03000 rw-p 41403000 00:00 0 
...
内存分配还是一样的,原本以为成为了垃圾的栈空间又被重新利用上了。
这是为什么呢?首先,我们使用的线程库(NPTL)是glibc的一部分;而我们调用的fork也是被glibc封装过的系统调用。glibc知道你要fork了,也知道fork之后会在进程B里面留下一堆垃圾(进程A中的线程栈),于是就在进程B中将这些垃圾管理了起来。当进程B需要创建线程、需要分配线程栈时,就能把这些垃圾重复利用。(具体可以从glibc的源码中找到答案,不过glibc源码可读性实在太差了点,就不列举了。)

我们再把fork函数换一换,不要使用glibc封装过的,直接使用系统调用(调用syscall(__NR_fork))。
$ cat /proc/B/maps
...
00501000-00522000 rwxp 00501000 00:00 0 
40000000-40001000 ---p 40000000 00:00 0 
40001000-40a01000 rw-p 40001000 00:00 0 
40a01000-40a02000 ---p 40a01000 00:00 0 
40a02000-41402000 rw-p 40a02000 00:00 0 
41402000-41403000 ---p 41402000 00:00 0 
41403000-41e03000 rw-p 41403000 00:00 0 
41e03000-41e04000 ---p 41e03000 00:00 0 
41e04000-42804000 rw-p 41e04000 00:00 0 
42804000-42805000 ---p 42804000 00:00 0 
42805000-43205000 rw-p 42805000 00:00 0 
43205000-43206000 ---p 43205000 00:00 0 
43206000-43c06000 rw-p 43206000 00:00 0 
...
果然,fork不经过glibc,glibc就不知道可以将进程A中的那些线程栈回收,在进程B中这些线程栈就真正成了垃圾。

测试程序:
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <stdlib.h>
#include <linux/unistd.h>
void *func(void *param) {
    sleep(10000);
}
void create_threads(int n) {
    pthread_t t;
    for (int i = 0; i < n; i++)
        pthread_create(&t, 0, func, 0);
}
int main(int argc, char* argv[]) {
    if (argc != 4) {
        printf("usage: %s (parent_thread_num) (child_thread_num) (is_direct_fork)\n", argv[0]);
        return 0;
    }
    create_threads(atoi(argv[1]));
    if (!(*argv[3] == '1' ? syscall(__NR_fork) : fork())) {
        sleep(10);
        create_threads(atoi(argv[2]));
    }
    sleep(10000);
    return 0;
}