谷歌 网站做推广,长沙民政计算机网站建设,企业方案项目策划书怎么写,体育西网站开发定制大家好#xff0c;推荐飞哥的一篇文章#xff01;关于进程和线程#xff0c;在 Linux 中是一对儿很核心的概念。但是进程和线程到底有啥联系#xff0c;又有啥区别#xff0c;很多人还都没有搞清楚。在网上对进程和线程的讨论中#xff0c;很多都是聚集在这二位有啥不同。…大家好推荐飞哥的一篇文章关于进程和线程在 Linux 中是一对儿很核心的概念。但是进程和线程到底有啥联系又有啥区别很多人还都没有搞清楚。在网上对进程和线程的讨论中很多都是聚集在这二位有啥不同。但事实在 Linux 上进程和线程的相同点要远远大于不同点。在 Linux 下的线程甚至都被称为了轻量级进程。我今天就给大家从 Linux 内核实现的角度给大家深度对比下进程和线程。一、线程的创建方法在 Redis 6.0 以上的版本里也开始支持使用多线程来提供核心服务我们就以它为例。在 Redis 主线程启动以后会调用 initThreadedIO 来创建多个 io 线程。redis 源码地址https://github.com/redis/redis//file:src/networking.c
void initThreadedIO(void) {//开始 io 线程的创建for (int i 0; i server.io_threads_num; i) {pthread_t tid;pthread_create(tid,NULL,IOThreadMain,(void*)(long)i)io_threads[i] tid;}
}创建线程具体调用的是 pthread_create 函数pthread_create 是在 glibc 库中实现的。在 glibc 库中pthread_create 函数的实现调用路径是 __pthread_create_2_1 - create_thread。其中 create_thread 这个函数比较重要它设置了创建线程时使用的各种 flag 标记。//file:nptl/sysdeps/pthread/createthread.c
static int
create_thread (struct pthread *pd, ...)
{int clone_flags (CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGNAL| CLONE_SETTLS | CLONE_PARENT_SETTID| CLONE_CHILD_CLEARTID | CLONE_SYSVSEM| 0);int res do_clone (pd, attr, clone_flags, start_thread,STACK_VARIABLES_ARGS, 1);...
}在上面的代码中传入参数中的各个 flag 标记是非常关键的。这里我们先知道一下传入了 CLONE_VM、CLONE_FS、CLONE_FILES 等标记就行了后面我们会讲内核中针对这些参数做的特殊处理。接下来的 do_clone 最终会调用一段汇编程序在汇编里进入 clone 系统调用之后会进入内核中进行处理。//file:sysdeps/unix/sysv/linux/i386/clone.S
ENTRY (BP_SYM (__clone))...movl $SYS_ify(clone),%eax...二、内核中对线程的表示在开始介绍线程的创建过程之前先给大家看看内核中表示线程的数据结构。开篇的时候我说了进程和线程的相同点要远远大于不同点。主要依据就是在 Linux 中无论进程还是线程都是抽象成了 task 任务在源码里都是用 task_struct 结构来实现的。我们来看 task_struct 具体的定义它位于 include/linux/sched.h//file:include/linux/sched.h
struct task_struct {//1.1 task状态 volatile long state;//1.2 进程线程的pidpid_t pid;pid_t tgid;//1.3 task树关系父进程、子进程、兄弟进程struct task_struct __rcu *parent;struct list_head children; struct list_head sibling;struct task_struct *group_leader; //1.4 task调度优先级int prio, static_prio, normal_prio;unsigned int rt_priority;//1.5 地址空间struct mm_struct *mm, *active_mm;//1.6 文件系统信息当前目录等struct fs_struct *fs;//1.7 打开的文件信息struct files_struct *files;//1.8 namespaces struct nsproxy *nsproxy;...
}这个数据结构已经在上一篇文章《Linux进程是如何创建出来的》中我们详细介绍过了。对于线程来讲所有的字段都是和进程一样的本来就是一个结构体来表示的。包括状态、pid、task 树关系、地址空间、文件系统信息、打开的文件信息等等字段线程也都有。这也就是我前面说的进程和线程的相同点要远远大于不同点本质上是同一个东西都是一个 task_struct 正因为进程线程如此之相像所以在 Linux 下的线程还有另外一个名字叫轻量级进程。至于说轻量在哪儿稍后我们再说。这里我们稍微说一下 pid 和 tgid 这两个字段。在 Linux 中每一个 task_struct 都需要被唯一的标识它的 pid 就是唯一标识号。//file:include/linux/sched.h
struct task_struct {......pid_t pid;pid_t tgid;
}对于进程来说这个 pid 就是我们平时常说的进程 pid。对于线程来说我们假如一个进程下创建了多个线程出来。那么每个线程的 pid 都是不同的。但是我们一般又需要记录线程是属于哪个进程的。这时候tgid 就派上用场了通过 tgid 字段来表示自己所归属的进程 ID。这样内核通过 tgid 可以知道线程属于哪个进程。三、线程创建过程要想知道进程和线程的区别到底在哪儿我们从线程的创建过程来详细看一下。3.1 回顾进程创建在《Linux进程是如何创建出来的》一文中我们了解了进程的创建过程。事实上进程线程创建的时候使用的函数看起来不一样。但实际在底层实现上最终都是使用同一个函数来实现的。我们再简单回顾一下创建进程时 fork 系统调用的源码fork 调用主要就是执行了 do_fork 函数。注意fork 函数调用 do_fork 的传的参数分别是SIGCHLD、0,0,NULL,NULL。//file:kernel/fork.c
SYSCALL_DEFINE0(fork)
{return do_fork(SIGCHLD, 0, 0, NULL, NULL);
}do_fork 函数又调用 copy_process 完成进程的创建。//file:kernel/fork.c
long do_fork(...)
{//复制一个 task_struct 出来struct task_struct *p;p copy_process(clone_flags, ...);...
}3.2 线程的创建我们在本文第一小节里介绍到 lib 库函数 pthread_create 会调用到 clone 系统调用为其传入了一组 flag。//file:nptl/sysdeps/pthread/createthread.c
static int
create_thread (struct pthread *pd, ...)
{int clone_flags (CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGNAL| CLONE_SETTLS | CLONE_PARENT_SETTID| CLONE_CHILD_CLEARTID | CLONE_SYSVSEM| 0);int res do_clone (pd, attr, clone_flags, ...);...
}好我们找到 clone 系统调用的实现。//file:kernel/fork.c
SYSCALL_DEFINE5(clone, ......)
{return do_fork(clone_flags, newsp, 0, parent_tidptr, child_tidptr);
}同样do_fork 函数还是会执行到 copy_process 来完成实际的创建。3.3 进程线程创建异同可见和创建进程时使用的 fork 系统调用相比创建线程的 clone 系统调用几乎和 fork 差不多也一样使用的是内核里的 do_fork 函数最后走到 copy_process 来完整创建。不过创建过程的区别是二者在调用 do_fork 时传入的 clone_flags 里的标记不一样。创建进程时的 flag仅有一个 SIGCHLD创建线程时的 flag包括 CLONE_VM、CLONE_FS、CLONE_FILES、CLONE_SIGNAL、CLONE_SETTLS、CLONE_PARENT_SETTID、CLONE_CHILD_CLEARTID、CLONE_SYSVSEM。关于这些 flag 的含义我们选几个关键的做一个简单的介绍后面介绍 do_fork 细节的时候会再次涉及到。CLONE_VM: 新 task 和父进程共享地址空间CLONE_FS新 task 和父进程共享文件系统信息CLONE_FILES新 task 和父进程共享文件描述符表这些 flag 会对 task_struct 产生啥影响我们接着看接下来的内容。四、揭秘 do_fork 系统调用在本节中我们以动态的视角来看一下线程的创建过程.前面我们看到进程和线程创建都是调用内核中的 do_fork 函数来执行的。在 do_fork 的实现中核心是一个 copy_process 函数它以拷贝父进程线程的方式来生成一个新的 task_struct 出来。//file:kernel/fork.c
long do_fork(unsigned long clone_flags, ...)
{//复制一个 task_struct 出来struct task_struct *p;p copy_process(clone_flags, stack_start, stack_size,child_tidptr, NULL, trace);//子任务加入到就绪队列中去等待调度器调度wake_up_new_task(p);...
}在创建完毕后调用 wake_up_new_task 将新创建的任务添加到就绪队列中等待调度器调度执行。这个代码很长我对其进行了一定程度的精简。//file:kernel/fork.c
static struct task_struct *copy_process(...)
{//4.1 复制进程 task_struct 结构体struct task_struct *p;p dup_task_struct(current);...//4.2 拷贝 files_structretval copy_files(clone_flags, p);//4.3 拷贝 fs_structretval copy_fs(clone_flags, p);//4.4 拷贝 mm_structretval copy_mm(clone_flags, p);//4.5 拷贝进程的命名空间 nsproxyretval copy_namespaces(clone_flags, p);//4.6 申请 pid 设置进程号pid alloc_pid(p-nsproxy-pid_ns);p-pid pid_nr(pid);p-tgid p-pid;if (clone_flags CLONE_THREAD)p-tgid current-tgid;......
}可见copy_process 先是复制了一个新的 task_struct 出来然后调用 copy_xxx 系列的函数对 task_struct 中的各种核心对象进行拷贝处理还申请了 pid 。接下来我们分小节来查看该函数的每一个细节。4.1 复制 task_struct 结构体注意一下上面调用 dup_task_struct 时传入的参数是 current它表示的是当前任务。在 dup_task_struct 里会申请一个新的 task_struct 内核对象然后将当前任务复制给它。需要注意的是这次拷贝只会拷贝 task_struct 结构体本身它内部包含的 mm_struct 等成员不会被复制。我们来简单看下具体的代码。//file:kernel/fork.c
static struct task_struct *dup_task_struct(struct task_struct *orig)
{//申请 task_struct 内核对象tsk alloc_task_struct_node(node);//复制 task_structerr arch_dup_task_struct(tsk, orig);...
}其中 alloc_task_struct_node 用于在 slab 内核内存管理区中申请一块内存出来。关于 slab 机制请参考- 内核内存管理//file:kernel/fork.c
static struct kmem_cache *task_struct_cachep;
static inline struct task_struct *alloc_task_struct_node(int node)
{return kmem_cache_alloc_node(task_struct_cachep, GFP_KERNEL, node);
}申请完内存后调用 arch_dup_task_struct 进行内存拷贝。//file:kernel/fork.c
int arch_dup_task_struct(struct task_struct *dst,struct task_struct *src)
{*dst *src;return 0;
}4.2 拷贝打开文件列表我们先回忆一下前面的内容创建线程调用 clone 系统调用的时候传入了一堆的 flag其中有一个就是 CLONE_FILES。如果传入了 CLONE_FILES 标记就会复用当前进程的打开文件列表 - files 成员。对于创建进程来讲没有传入这个标志就会新创建一个 files 成员出来。好了我们继续看 copy_files 具体实现。//file:kernel/fork.c
static int copy_files(unsigned long clone_flags, struct task_struct *tsk)
{struct files_struct *oldf, *newf;oldf current-files;if (clone_flags CLONE_FILES) {atomic_inc(oldf-count);goto out;}newf dup_fd(oldf, error);tsk-files newf;...
}从代码看出如果指定了 CLONE_FILES创建线程的时候只是在原有的 files_struct 里面 1 就算是完事了指针不变仍然是复用创建它的进程的 files_struct 对象。这就是进程和线程的其中一个区别对于进程来讲每一个进程都需要独立的 files_struct。但是对于线程来讲它是和创建它的线程复用 files_struct 的。4.3 拷贝文件目录信息再回忆一下创建线程的时候传入的 flag 里也包括 CLONE_FS。如果指定了这个标志就会复用当前进程的文件目录 - fs 成员。对于创建进程来讲没有传入这个标志就会新创建一个 fs 出来。好我们继续看 copy_fs 的实现。//file:kernel/fork.c
static int copy_fs(unsigned long clone_flags, struct task_struct *tsk)
{struct fs_struct *fs current-fs;if (clone_flags CLONE_FS) {fs-users;return 0;}tsk-fs copy_fs_struct(fs);return 0;
}和 copy_files 函数类似在 copy_fs 中如果指定了 CLONE_FS创建线程的时候并没有真正申请独立的 fs_struct 出来近几年只是在原有的 fs 里的 users 1 就算是完事。而在创建进程的时候由于没有传递这个标志会进入到 copy_fs_struct 函数中申请新的 fs_struct 并进行赋值拷贝。4.4 拷贝内存地址空间创建线程的时候带了 CLONE_VM 标志而创建进程的时候没带。接下来在 copy_mm 函数 中会根据是否有这个标志来决定是该和当前线程共享一份地址空间 mm_struct还是创建一份新的。//file:kernel/fork.c
static int copy_mm(unsigned long clone_flags, struct task_struct *tsk)
{struct mm_struct *mm, *oldmm;oldmm current-mm;if (clone_flags CLONE_VM) {atomic_inc(oldmm-mm_users);mm oldmm;goto good_mm;}mm dup_mm(tsk);
good_mm:return 0;
}对于线程来讲由于传入了 CLONE_VM 标记所以不会申请新的 mm_struct 出来而是共享其父进程的。多线程程序中的所有线程都会共享其父进程的地址空间。而对于多进程程序来说每一个进程都有独立的 mm_struct(地址空间)。因为在内核中线程和进程都是用 task_struct 来表示只不过线程和进程的区别是会和创建它的父进程共享打开文件列表、目录信息、虚拟地址空间等数据结构会更轻量一些。所以在 Linux 下的线程也叫轻量级进程。在打开文件列表、目录信息、内存虚拟地址空间中内存虚拟地址空间是最重要的。因此区分一个 Task 任务该叫线程还是该叫进程一般习惯上就看它是否有独立的地址空间。如果有就叫做进程没有就叫做线程。这里展开多说一句对于内核任务来说无论有多少个任务其使用地址空间都是同一个。所以一般都叫内核线程而不是内核进程。五 结论创建线程的整个过程我们就介绍完了。回头总结一下对于线程来讲其地址空间 mm_struct、目录信息 fs_struct、打开文件列表 files_struct 都是和创建它的任务共享的。但是对于进程来讲地址空间 mm_struct、挂载点 fs_struct、打开文件列表 files_struct 都要是独立拥有的都需要去申请内存并初始化它们。总之在 Linux 内核中并没有对线程做特殊处理还是由 task_struct 来管理。从内核的角度看用户态的线程本质上还是一个进程。只不过和普通进程比稍微“轻量”了那么一些。那么线程具体能轻量多少呢我之前曾经做过一个进程和线程的上下文切换开销测试。进程的测试结果是一次上下文切换平均 2.7 - 5.48 us 之间。线程上下文切换是 3.8 us左右。总的来说进程线程切换还是没差太多。参见《进程/线程切换究竟需要多少开销》