这是我参与「第三届青训营 -后端场」笔记创作活动的第5篇笔记
在青训营课程中学习了go语言,Go语言内置了协程,协程本质上是用户自定义的线程。那么线程在Linux内核是如何工作的呢?他与进程又有什么区别?本文带你一探究竟!
首先说些大家肯定都知道的:
- 线程有自己的全局数据
- 进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。
这样说,肯定是背诵的八股文,本文带你从内核角度重新认识线程和进程!
在linux内核角度出发的话,线程和进程都被看做是一个任务,统一被task_struct管理,链表将所有的task_struct串起来。task_struct 中,使用pid(线程id),tgid(进程的主线程id)可以区分进程和线程。struct task_struct有保存有关线程/进程中的一切信息,主要包括有线程/进程状态、与其他线程/进程关系、虚拟内存相关、日志相关、线程/进程限制等。
进程还是线程的创建都是调用系统调用接口实现的。创建的主要工作实际上就是创建task_strcut结构体,并将该对象添加至工作队列中去。而线程和进程在创建时,通过传递flag的不同,而选择不同的方式共享父进程/父线程的资源,从而造成在用户空间所看到的进程和线程的不同。无论采用何种方式创建进程和线程,最终都会调用do_fork接口。
do_fork主要做了三件事,
1 copy_process
2 确定PID
3 wake_up_new_task
wake_up_new_task即是将新创建的线程/进程添加至调度程序的队列中 do_fork主要的一部分工作集中在copy_process中,线程与进程之间的区别也是在该接口中体现,接口的代码流程图如下所示:
当上层以pthread_create接口call到kernel时,clone_flag是有CLONE_PTHREAD标识
CLONE_PTHREAD标识在最后两个步骤(设置各个ID、进程关系)时体现:(current为当前进程/线程的task_struct结构体 ,p为新创建的结构体对象)
最后一步:
if (clone_flags & CLONE_THREAD) {
p->group_leader = current->group_leader;
p->tgid = current->tgid;
} else {
p->group_leader = p;
p->tgid = p->pid;
}
在这些形如copy_abc的接口中,通过判断该flag标识,决定对内核子系统资源是与父进程/线程公用还是新创建出来。可参考下图。
一开始父进程和子进程对于res_abc指向同一个内容(通过dup_task_struct接口实现,子进程完全copy父进程),然后经过copy_abc程序,当有CLONE_abc标识时,父进程会共享资源,同时res_abc的引用计数+1,当!CLONE_abc时,会创建一个res_abc的副本。