1. 基本概念
Linux内核中没有使用单独的数据结构来描述进程和线程,而是将它们统一起来,使用task_struct结构体来描述,这就是“Linux不区分进程和线程”的来源。
内核态: 所有线程都由kthreadd创建,运行在内核态,由独立的task_sturct描述。内核中不区分进程和线程,为了方便区分,这里统称为内核线程
用户态:
- 单线程进程:仅有一个线程,也就是主线程,内核中由一个task_struct描述,进程id就是task_struct.pid。
- 多线程进程:包含多个线程,每个线程都有自己的task_struct,有几个线程(包含主线程)就会有几个task_struct,所有task_struct的group_leader指向主线程的task_struct,进程id就是主线程task_struct.pid,所有线程的tgid都指向进程id。
1.1. 进程
进程是Linux系统中最基本的概念,它是资源分配的最小单位。
进程的创建、运行、结束都在内核态进行,进程的描述在内核态中由task_struct描述。
1.2. 线程
线程是进程的一个执行单元,它是操作系统调度和执行的最小单位。
与进程一样,线程的创建、运行、结束都在用户态进行,线程的描述在用户态中由thread_struct描述。
1.3. 进程组、会话
进程组:进程组是一组相关进程,它们共享一个进程组ID,进程组ID默认是进程组内第一个进程的task_struct.pid。
会话:会话是一组相关进程组,它们共享一个会话ID,会话ID是会话内第一个进程的 task_struct.pid
。
2. 进程关系
可以通过ps命令来查看进程间的关系。
1ps -Teo ppid,sid,pgid,pid,tgid,tid,comm
注意ps显示的pid和tgid,在内核中含义是相同的。
2.1. 父子、兄弟
以内核最开始创建的swapper、user_init、kthreadd三个进程(线程)为例,
- swapper是user_init和ktheread的父进程。
- user_init和ktheread的task_struct.parent和task_struct.real_parent指向swapper的task_struct。
- user_init和ktheread的task_struct.sibling挂到swapper的task_struct.children链表。
- user_init和ktheread的task_struct.tasks挂到swapper的task_struct.tasks链表。
- user_init和kthreadd是兄弟进程。
- 进程可以遍历自己的task_struct.sibling链表,找到兄弟进程和父进程。
2.2. 进程、线程、线程组
以user_multi_thread为例,
- user_multi_thread_leader是进程的主线程,主线程的task_struct.pid就是进程id,task_struct.tgid与task_struct.pid相同,task_struct.group_leader指向自身。
- 只有主线程user_multi_thread_leader的task_struct.tasks会挂到init_task.tasks链表。
- 只有主线程user_multi_thread_leader的task_struct.sibling会挂到父进程的task_struct.children链表。
- user_multi_thread包含两个线程,分别是user_multi_thread1和user_multi_thread2,每个线程都有自己的task_struct,线程id为task_struct.pid,task_struct.tgid为进程id。
- user_multi_thread1和user_multi_thread2的task_struct.real_parent与user_multi_thread_leader的task_struct.real_parent相同。
2.3. 进程组、会话
进程的进程组和会话关系是靠 struct signal_struct
的 pids
数组来建立的,通过 enum pid_type
进行索引。除了 init_signals.pids[PIDTYPE_PID]
会指向init_struct_pid外,其他task_stuct的 pids[PIDTYPE_PID]
都没有使用,而是使用了 task_struct.thread_pid
,具体可以看 task_pid_type
、init_task_pid
、task_pid_ptr
等函数。
1/// inlcude/linux/pid.h
2enum pid_type
3{
4 PIDTYPE_PID, /// 线程id,这里是内核态PID的概念
5 PIDTYPE_TGID, /// 线程组id,对应用户态PID,也就是用户进程id
6 PIDTYPE_PGID, /// 进程组id
7 PIDTYPE_SID, /// 会话id
8 PIDTYPE_MAX,
9};
进程在创建时,默认都是继承了父进程的sid、pgid,之后再通过系统调用,来创建或加入新的会话或者进程组。进程组是属于会话的,当进程改变所属的会话时,进程组id自动改为新的会话id。
2.3.1. 内核线程的会话和进程组
swapper线程和所有内核线程的会话和进程组都指向 init_struct_pid
。
1 PPID SID PGID PID TGID TID COMMAND
2 0 1 1 1 1 1 systemd
3 0 0 0 2 2 2 kthreadd
4 2 0 0 3 3 3 rcu_gp
5 2 0 0 4 4 4 rcu_par_gp
2.3.2. 用户进程的会话和进程组
用户init进程(1号进程)在创建时,继承 init_struct_pid
,但是通常来说,init进程会创建新的会话和进程组。其他用户线程大多也会创建自己的会话和进程组。而自己用C语言写的程序,通常不会改变会话id,此时进程和执行程序的shell属于同一会话。
2.3.3. 用户多线程示例
multi_thread是用pthread_create创建的线程,所有线程的sid、pgid和pid是相同的。
1 PPID SID PGID PID TGID TID COMMAND
2 3607 3608 3608 3608 3608 3608 bash /// bash会话
3 3608 3608 218513 218513 218513 218513 multi_thread /// 主进程(主线程)
4 3608 3608 218513 218513 218513 218514 multi_thread /// 子线程
5 3608 3608 218513 218513 218513 218515 multi_thread
6 3608 3608 218513 218513 218513 218516 multi_thread
2.3.4. 用户进程组示例
比较典型的是sid为135990,pgid为215368的python进程,这是通过python的multiporces库实现的多进程。所有子进程的sid和pgid是相同的。
1 PPID SID PGID PID TGID TID COMMAND
2 16502 135990 135990 135990 135990 135990 bash /// bash会话
3 135990 135990 215368 215368 215368 215368 python3 /// 进程组
4 215368 135990 215368 215369 215369 215369 python3 /// 子进程
5 215368 135990 215368 215372 215372 215372 python3 /// 子进程
6 215368 135990 215368 215375 215375 215375 python3
7 215368 135990 215368 215378 215378 215378 python3
8 215368 135990 215368 215381 215381 215381 python3
9 215368 135990 215368 215384 215384 215384 python3
10 215368 135990 215368 215393 215393 215393 python3
11 215368 135990 215368 215398 215398 215398 python3
12 215368 135990 215368 215400 215400 215400 python3
13 215368 135990 215368 215407 215407 215407 python3
14 215368 135990 215368 215414 215414 215414 python3
15 215368 135990 215368 215418 215418 215418 python3
16 215368 135990 215368 215423 215423 215423 python3
17 215368 135990 215368 215427 215427 215427 python3
18 215368 135990 215368 215437 215437 215437 python3
19 215368 135990 215368 215446 215446 215446 python3
再有就是make,make实际执行的是使用qemu启动一个虚拟机。这里sid对应的是执行make的bash,pgid对应的是make的进程组。sh和qemu的pid不同,但sid和pgid是相同的。
1 PPID SID PGID PID TGID TID COMMAND
2 3588 3589 3589 3589 3589 3589 bash
3 3589 3589 216041 216041 216041 216041 make
4 216041 3589 216041 216965 216965 216965 sh
5 216965 3589 216041 216966 216966 216966 qemu-system-aar
6 216965 3589 216041 216966 216966 216967 qemu-system-aar
7 216965 3589 216041 216966 216966 216968 qemu-system-aar
8 216965 3589 216041 216966 216966 216969 qemu-system-aar
3. 系统调用
1pid_t gettid(void); /// 获取线程id,对应内核态pid
2
3pid_t getpid(void); /// 获取进程id,对应内核态tgid
4pid_t getppid(void); /// 获取父进程id
5
6/// 修改进程所属的进程组
7int setpgid(pid_t pid, pid_t pgid);
8pid_t getpgid(pid_t pid);
9
10pid_t getpgrp(void); /* POSIX.1 version */
11[[deprecated]] pid_t getpgrp(pid_t pid); /* BSD version */
12
13/// 基于当前进程创建一个新的进程组
14int setpgrp(void); /* System V version */
15[[deprecated]] int setpgrp(pid_t pid, pid_t pgid); /* BSD version */
16
17pid_t getsid(pid_t pid);
18/// 基于当前进程创建一个新的会话,会话id是当前进程主线程的task_struct.pid,同时创建进程组
19pid_t setsid(void);
4. 内核源码中的解释
4.1. struct pid是什么
1/// inlcude/linux/pid.h
2/*
3 * What is struct pid?
4 *
5 * A struct pid is the kernel's internal notion of a process identifier.
6 * It refers to individual tasks, process groups, and sessions. While
7 * there are processes attached to it the struct pid lives in a hash
8 * table, so it and then the processes that it refers to can be found
9 * quickly from the numeric pid value. The attached processes may be
10 * quickly accessed by following pointers from struct pid.
11 *
12 * Storing pid_t values in the kernel and referring to them later has a
13 * problem. The process originally with that pid may have exited and the
14 * pid allocator wrapped, and another process could have come along
15 * and been assigned that pid.
16 *
17 * Referring to user space processes by holding a reference to struct
18 * task_struct has a problem. When the user space process exits
19 * the now useless task_struct is still kept. A task_struct plus a
20 * stack consumes around 10K of low kernel memory. More precisely
21 * this is THREAD_SIZE + sizeof(struct task_struct). By comparison
22 * a struct pid is about 64 bytes.
23 *
24 * Holding a reference to struct pid solves both of these problems.
25 * It is small so holding a reference does not consume a lot of
26 * resources, and since a new struct pid is allocated when the numeric pid
27 * value is reused (when pids wrap around) we don't mistakenly refer to new
28 * processes.
29 */
struct pid
是内核对进程标识符的内部概念。它指的是单个任务、流程组和会话。虽然有进程附加到它,但 struct pid
位于哈希表中,因此可以从数字pid值中快速找到它以及它所引用的进程。可以通过以下来自 struct pid
的指针快速访问附加的进程。
在内核中存储pid_t值并在以后引用这些值是有问题的。最初具有该pid的进程可能已经退出并包装了pid分配器,而另一个进程可能已经出现并被分配了该pid。
通过持有对 struct task_struct
的引用来引用用户空间进程存在问题。当用户空间进程退出时,现在无用的 task_struct
仍然保留。task_struct加上一个堆栈会消耗大约10K的低内核内存。更确切地说,这是 THREAD_SIZE + sizeof(struct task_struct)
。相比之下,struct pid
大约为64个字节。
持有对 struct pid
的引用可以解决这两个问题。它很小,因此持有引用不会消耗大量资源,而且由于在重复使用数字pid值时(当pid换行时)会分配新的 struct pid
,所以我们不会错误地引用新的进程。
1/// inlcude/linux/pid.h
2/*
3 * struct upid is used to get the id of the struct pid, as it is
4 * seen in particular namespace. Later the struct pid is found with
5 * find_pid_ns() using the int nr and struct pid_namespace *ns.
6 */
struct upid
用于获取在特定命名空间中 struct pid
的id,find_pid_ns
使用 int nr
和 struct pid_namespace *ns
找到 struct pid
。