从内核角度看Linux 线程和进程的区别
多数人都会讲说线程和进程在内核中是相同的,没有严格地做区分。这样讲是没错了,但对于应用开发者来说,这样讲是有点笼统。本文将从内核角度,分析线程和进程之间的区别,希望能对这一块感兴趣的人提供借鉴意义。
1 数据结构 task_struct
Linux中无论是进程还是线程,只要是调度单元,都通过 struct task_struct表示。这也是为什么讲说进程和线程在内核相同的原因。
struct task_struct有保存有关线程/进程中的一切信息,主要包括有线程/进程状态、与其他线程/进程关系、虚拟内存相关、日志相关、线程/进程限制等。该结构体定义在include/linux/sched.h文件中,感兴趣可以详细阅读
那么,进程和线程在task_struct结构体中是否有标识上的不同?
实际上,在struct task_struct中并没有明确的标识(枚举类型),区分该task是线程还是进程,不过可以通过pid和tgid简单判断当前task是哪种类型。
在该结构体中如下段code所示,全局pid和tgid保存在task_struct结构体中。pid_t一般为int型,即可以同时使用不同标识的id。
pid用于标识不同进程和线程。
tgid用于标识线程组id,在同一进程中的所有线程具有同一tgid。tgid值等于进程第一个线程(主线程)的pid值。接着以CLONE_THREAD来调用clone建立的线程,都具有同样的tgid。(后文会详细描述创建过程)
group_leader 线程组中的主线程的task_struct指针。
struct task_struct {
...
pid_t pid;
pid_t tgid;
...
struct *group_leader;
}
那么除了tgid和group_leadr是进程/线程的区别外,还有什么其他的区别么?
进程还是线程的创建都是由父进程/父线程调用系统调用接口实现的。创建的主要工作实际上就是创建task_strcut结构体,并将该对象添加至工作队列中去。而线程和进程在创建时,通过CLONE_THREAD flag的不同,而选择不同的方式共享父进程/父线程的资源,从而造成在用户空间所看到的进程和线程的不同。
2 线程/进程的创建
无论以何种方式创建线程/进程在Linux kernel最终都是调用do_fork接口(定义在kernel/fork.c)
其函数原型为:
long do_fork(unsigned long clone_flags,
unsigned long stack_start,
unsigned long stack_size,
int __user *parent_tidptr,
int __user *child_tidptr)
- clone_flags是一个标志集合,用来指定控制复制过程的一些属性。最低字节指定了在子进程终止时被发给父进程的信号号码。其余的高位字节保存了各种常数。
- stack_start是用户状态下栈的起始地址。
- stack_size是用户状态下栈的大小。
- arent_tidptr和child_tidptr是指向用户空间中地址的两个指针,分别指向父子进程的PID。NPTL(Native Posix Threads Library)库的线程实现需要这两个参数。
do_fork的代码流程图如下所示:
上面流程特别判断了是否是vfork,该接口是vfork接口call下来,在子进程没有执行完前,父进程处于阻塞态。一般用于子进程直接调用execv时使用。因为子进程不需要copy父进程的资源从而减少do_fork时的消耗,不过由于fork增加了写时复制机制,vfork也很少使用。这些不是这篇介绍的重点。
那么拿掉vfork的过程,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;
}
那么,以我们的理解来看,线程会共享信号、共享虚拟地址空间...又以什么体现呢?
去glibc查询了pthread_create的实现,当call到kernel时的clone_flag如下:
const int clone_flags = (CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SYSVSEM
| CLONE_SIGHAND | CLONE_THREAD
| CLONE_SETTLS | CLONE_PARENT_SETTID
| CLONE_CHILD_CLEARTID
| 0);
所以在创建线程时,clone_flags有其他许多项共同构成,才让我们看出来最终线程与进程间的不同。这些flag主要体现在【分享/复制进程各个部分中】步骤。
这些CLONE_abc的使用方法相似。在这些形如copy_abc的接口中,通过判断该flag标识,决定对内核子系统资源是与父进程/线程公用还是新创建出来。可参考下图。
一开始父进程和子进程对于res_abc指向同一个内容(通过dup_task_struct接口实现,子进程完全copy父进程),然后经过copy_abc程序,当有CLONE_abc标识时,父进程会共享资源,同时res_abc的引用计数+1,当!CLONE_abc时,会创建一个res_abc的副本。