在多线程程序里面fork
在多线程程序里面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;
}
进程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;
}