NPTL内核任务、用户线程和进程
本篇主要描述内核任务、用户线程、进程三者之间的关系。这三个术语在我们日常的使用中,经常概念混淆。其主要原因,往往是我们没有将用户线程与内核任务建立起对应的联系。
我们对这三个名词进行官方一点的描述。
进程:用于在用户空间指向正在执行的程序示例。一个进程通常通过一组fork()和exec()函数调用来生成新的进程。初始进程调用fork()函数,产生一个子进程。子进程继承了母进程的整个执行环境。fork()函数调用将子进程的进程标识(PID)反馈给母进程,同时还包括子进程中的PID。然后,子进程调用使用exec()函数调用来执行其他的命令,改变继承来的执行环境。同时,母进程或者迅速退出,或者等待子进程回到其初始状态。
内核任务:用户进程在内核中由任务实现,对应内核数据结构中的struct task_struct;一个用户进程必然会对应一个内核任务。
而用户线程相对比较复杂,它在linux的发展过程中,发生过一系列版本的改变。它的模型分以下三类,1:1,N:1和N:M。但无论是哪种模型,用户线程与生成线程的进程是共享代码的,并且用户线程和用户进程在内核中,均由task_struct结构体管理。
注:目前Linux使用1:1的NPTL,并且已经完全集成到Glibc中了。
1. 三类模型
1.1. 每种模型的特点
模型
说明
1:1
①内核级线程(kernel-level threading)
②将用户生成的线程与内核中调度的实体1:1映射
③简化线程实现
④在Solaris、NetBSD、FreeBSD中使用1:1模型
N:1
①用户级线程(user-level threading)
②所有应用程序级线程映射为1个内核级调度实体
③内核完全不具有应用程序线程信息
④可以快速执行上下文切换,但无法获得多线程处理器(multi-thread processor)或多处理器计算机(multi-process computer)等硬件上的优点。
⑤无法同时调度多个线程。
N:M
①混合线程(hybrid thread)
②将N个应用程序线程映射到可执行M个调度的内核级线程
③线程库(thread library)负责调度用户线程,不执行系统调用,故可快速执行上下文切换
④难以实现,且优先顺序可能改变
⑤需要管理用户空间调度和内核空间调度
1.2. 每种模型对应的实现方式
1:1模型实现于LinuxThread和之后的NPTL(Native POSIX Thread Library,本地POSIX线程库)
N:1模型基于GNU便携式线程(Portable Thread)。它是基于POSIX/ANSI-C的用户空间线程库。对各线程的调度均通过GNU Pth库实现,内核无法认知用户空间线程。该特性使得其无法活用SMP。
N:M模型实现于NGPT(Next Generation POSIX Threads,下一代POSIX线程)。该项目是由IBM主持,但在2003年停止开发。
由于,目前Linux使用的是1:1的模型,因而下面将对1:1的两种实现方式展开具体的描述。
2. LinuxThread和NPTL
LinuxThreads针对单一进程可产生的线程数通过一个编译器设置。此外,它还使用一个进程管理器协调每个进程产生的所有线程间的关系。这样会大大增加线程建立和消除占用的资源。尽管基本上每个线程都有独立的进程PID,但信号的处理仍然是在各个进程中完成的。由于种种原因,在LinuxThreads实施过程中,同时产生并工作的线程数量常常会受到限制。这些限制包括:
①由于LinuxThreads使用克隆系统调用(clone system call)生成新线程,每个线程拥有自己的PID,因此会发生信号处理问题。LinuxThread使用SIGUSR1和SIGUSR2管理线程,这也意味着程序无法使用这两个信号。
②由于LinuxThread并未支持真正的线程,而是利用克隆系统调用支持的。因而存在信号处理、调度、线程间同步的问题。
使用NPTL时,母进程下的各个线程都共享同一PID,因此getpid()函数将为进程中的所有线程返回同一PID。在NPTL下,每一个线程的线程ID在使用时必须是唯一的,这样才能对每个线程进程正确的识别。另外,NPTL遵循POSIX规定,以进程为单位处理信号。
举个栗子:发出信号SIGSTOP时,LinuxThreads只有接收该信号的线程停止,但NPTL中所有进程均会停止。
以下是官方给出的LinuxThread、NPTL和NGPL的性能比较数据。
可以明显地看出NPTL的性能要优于NGPT和LinuxThreads。
实际上,NPTL线程的实现基本是基于内核的。
在linux2.6之后(支持NPTL),内核有了线程组的概念, task_struct结构中增加了一个tgid(thread group id)字段。如果这个task是一个"主线程",则它的tgid等于pid,,否则tgid等于进程的pid(即主线程的pid)。
此外,每个线程有自己的pid。在clone系统调用中,传递CLONE_THREAD参数就可以把新进程的tgid设置为父进程的tgid(否则新进程的tgid会设为其自身的pid).类似的XXid在task_struct中还有两个:task->signal->pgid保存进程组的打头进程的pid、task->signal->session保存会话打头进程的pid。通过这两个id来关联进程组和会话。
有了tgid,内核或相关的shell程序就知道某个tast_struct是代表一个进程还是代表一个线程,也就知道在什么时候该展现它们,什么时候不该展现(比如在ps的时候, 线程就不要展现了),.而getpid(获取进程ID)系统调用返回的也是tast_struct中的tgid,而tast_struct中的pid则由gettid系统调用来返回.在执行ps命令的时候不展现子线程,也是有一些问题的。比如程序a.out运行时,创建了一个线程。假设主线程的pid是10001、子线程是10002(它们的tgid都是10001)。这时如果你kill 10002,是可以把10001和10002这两个线程一起杀死的,尽管执行ps命令的时候根本看不到10002这个进程。如果你不知道linux线程背后的故事,肯定会觉得遇到灵异事件了。
为了应付"发送给进程的信号"和"发送给线程的信号", task_struct里面维护了两套signal_pending,一套是线程组共享的,一套是线程独有的。通过kill发送的信号被放在线程组共享的signal_pending中, 可以由任意一个线程来处理;通过pthread_kill发送的信号(pthread_kill是pthread库的接口, 对应的系统调用中tkill)被放在线程独有的signal_pending中, 只能由本线程来处理.
当线程停止/继续, 或者是收到一个致命信号时, 内核会将处理动作施加到整个线程组中。
3. NPTL特点
3.1. NAPT的使用
使用getconf GNU_LIBPTHREAD_VERSION可以得到系统gcc在编译时支持的多线程方式。
1
2
3
root@skynet:/home/wangda# getconf GNU_LIBPTHREAD_VERSION
NPTL 2.21
这里表示支持NPTL 2.21版本。当然,如果返回的是linuxthreads,说明gcc不支持NPTL,而是linuxThreads
在嵌入式系统中,一般不会安装工具链,也可以直接运行libc.so.6查看系统的glibc是否使用的是NTPL thread,如下面红色部分所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
root@skynet:/# /lib/x86_64-linux-gnu/libc.so.6
GNU C Library (Ubuntu GLIBC 2.21-0ubuntu4) stable release version 2.21, by Roland McGrath et al.
Copyright (C) 2015 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.
There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE.
Compiled by GNU CC version 4.9.2.
Available extensions:
crypt add-on version 2.1 by Michael Glad and others
GNU Libidn by Simon Josefsson
Native POSIX Threads Library by Ulrich Drepper et al
BIND-8.2.3-T5B
libc ABIs: UNIQUE IFUNC
For bug reporting instructions, please see:
<https://bugs.launchpad.net/ubuntu/+source/glibc/+bugs>.
3.2. NAPT下线程呈现
NAPT下每个线程即对应内核一个stask_struct结构体,但pid与主进程相同。
这一点可以在系统上得到验证:
1
2
3
root@skynet:/home/wangda# ps -aux |grep 1888
wangda 18880.01.6 514284 16936 ? Sl 11:48 0:00 update-notifier
我们看到,该进程的pid为1888。
在/proc/<pid>/task下可以看到归属于该进程的线程号。
1
2
3
4
5
6
7
root@skynet:/home/wangda# ls -l /proc/1888/task/
total 0
dr-xr-xr-x 7 wangda wangda 0 12月 20 11:49 1888
dr-xr-xr-x 7 wangda wangda 0 12月 20 11:49 1891
dr-xr-xr-x 7 wangda wangda 0 12月 20 11:49 1892
dr-xr-xr-x 7 wangda wangda 0 12月 20 11:49 1893
以上在pid为1888的进程中,存在四个线程,分别是1888、1891、1892和1893。
遗留问题:
SMP的负载均衡是按照进程计数的,由于NPTL是1:1模型,每个线程对应一个struct task_struct,所以可以实现每个线程在不同的CPU之间进行调度。但问题是,内核或者NPTL是如何实现获取PID时,每个子线程获取到的是主进程的PID,并且实现资源的共享?
页:
[1]