linux中fork可以用于创建一个子进程,fork和excv系统调用可以创建一个新的进程。

clone系统调用也可以创建进程。

本实验探究fork()和pthread_create()在linux内核中分别调用了什么函数。

首先 看一下linux内核在执行fork()的时候底层的系统调用

#include <sys/types.h>
#include <unistd.h>
int main(){
    int pid=fork();
}

编译之后运行strace查看系统调用链

execve("./test", ["./test"], 0x7fff16415f70 /* 37 vars */) = 0
brk(NULL)                               = 0x564f505a7000
.....
clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7fb965888a10) = 4767
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=4767, si_uid=1000, si_status=0, si_utime=0, si_stime=0} ---
exit_group(0)                           = ?
+++ exited with 0 +++

可以看到有一行为

clone(child_stack=NULL,flags=CLONE_CH

ILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7fb965888a10) = 4767

然后看一下执行pthread_create函数linux内核的调用链。

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

void *fun1(){
    printf("a");
}

pthread_t p;
int main(){
      if (pthread_create(&p, NULL, fun1, NULL) != 0) {
        fprintf(stderr, "Error creating thread\n");
        return 1;
    }
}

编译后使用starce 追踪linux内核调用链。

execve("./test1", ["./test1"], 0x7ffe9aa808b0 /* 37 vars */) = 0
brk(NULL) = 0x56361cbb4000
............
clone3({flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID, child_tid=0x7f4b39f51910, parent_tid=0x7f4b39f51910, exit_signal=0, stack=0x7f4b39751000, stack_size=0x7fff00, tls=0x7f4b39f51640} => {parent_tid=[21193]}, 88) = 21193

使用pthread_create创建线程时还是调用了clone函数。

所以可得结论无论是fork()还是pthread_create底层都是调用了clone函数,所以linux中线程是一个轻量级进程。

下面解析一下创建线程时clone各个参数的含义:

CLONE_VM 表示虚拟内存空间被共享。
CLONE_FS 表示文件系统信息被共享。
CLONE_FILES 表示打开的文件描述符被共享。
CLONE_SIGHAND 表示信号处理函数被共享。
CLONE_THREAD 表示这是一个线程而不是独立的进程。
CLONE_SYSVSEM 表示共享 System V IPC semaphores。
CLONE_SETTLS 允许设置线程本地存储(TLS)指针。
CLONE_PARENT_SETTID 允许父进程通过写入到一个给定的地址来设置子线程的 ID。
CLONE_CHILD_CLEARTID 允许子线程通过写入到一个给定的地址来清除其线程 ID。

stack=0x7f4b39751000: 这个地址是新线程栈的基地址。

stack_size=0x7fff00: 这个值通常表示栈大小,但在这个上下文中,它看起来像是一个无效的值(可能是输出解析的问题),因为栈大小应该是字节的数量。

tls=0x7f4b39f51640: 这个地址是 TLS 块的地址。

从上面可以看到在创建线程时,有一个标志时表示创建的是线程,然后创建的线程和父进程共享虚拟内存空间,文件描述符,但是每个线程都有自己一个栈和TLS块,在线程初始化的时候设置栈指针、栈大小和TLS地址。

所以不同线程之间共享同一个虚拟内存空间,但是每个线程都有自己的栈和TLS块。

实验结论:

在使用fork时子进程需要复制父进程的mm_struct结构,页表等结构,并采用写时复制的策略使父进程和子进程有不同的地址空间。但是线程由于共享了内存空间和其他一些资源,所以线程上下文切换的开销省去了复制父进程内存空间和其他一些资源的开销,所以使用线程可以有更高的并发度。