从内核角度看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型,即可以同时使用2^{32}不同标识的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的副本。

又去glibc查询了fork的clone_flags

CLONE_CHILD_SETTID | CLONE_CHILD_CLEARTID | SIGCHLD

那么线程创建就比进程多CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SYSVSEM | CLONE_SIGHAND | CLONE_THREAD,从这些宏的字面意义上看,子线程会与父线程共享虚拟地址空间、文件、信号等。

每个flag的实现原理基本一致,上文已讨论过,这里仅对具体的哪些资源造成了影响进行分析。

CLONE_VM 为虚拟地址空间

所以子线程会共享父线程的虚拟地址空间(通过struct mm_struct *mm指向实例描述),active_mm当用户线程切换至内核线程时使用,这里不详述。

struct task_struct {
...
    struct mm_struct *mm;
    struct mm_struct *active_mm;
...
}

CLONE_FS struct fs_struct *fs 进程当前目录及工作目录

CLONE_SIGHAND struct sighand_struct *sighand 信号及信号处理函数

3 ps -H 命令的显示

发布了44 篇原创文章 · 获赞 13 · 访问量 5万+
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 编程工作室 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览