目录

Linux Kernel | Linux task_struct 结构体概述

1. task_struct 概述

在 Linux 内核中,无论是进程还是线程,到了内核里面,都叫做任务(Task),由统一的数据结构 task_struct 进行管理。task_struct 是 Linux 中的进程描述符,是感知进程存在的唯一实体。Linux 内核中通过一个双向循环链表将所有的 task_struct 串了起来。

不同的操作系统中,PCB 所包含的内容也会不同。

1.1. 任务 ID

1
2
3
4
// include\linux\sched.h
pid_t pid;
pid_t tgid;
struct task_struct *group_leader;

pid(process ID)是任务的唯一标识符,每一个任务的 pid 都是不一样的。也就是说,如果一个进程有多个线程,那么这些多个线程所使用的 pid 也都是不一样的。

tgid(thread group ID),是线程组的 ID,一个进程中的所有线程的 tgid 都是一样的。如果一个进程,只有主线程,那么 pid 是自己,tgid 也是自己。如果一个进程,创建了其他线程,那么其他线程都有自己的 pid,但是其他线程的 tgid 是进程主线程的 pid。

group_leader 则指向进程主线程的 task_struct。同上,如果一个进程,只有主线程,那么 pid 是自己,group_leader 指向的也是自己。如果一个进程,创建了其他线程,那么这些线程的 group_leader 指向的都是进程主线程的 task_struct。

有了 tgid 之后,我们就知道 task_struct 代表的是一个进程还是一个线程了。

1.2. 进程亲缘关系

Linux kernel 中主要用以下这些成员变量来表示进程的亲缘关系:

1
2
3
4
5
6
// include\linux\sched.h

struct task_struct __rcu *real_parent; /* real parent process */
struct task_struct __rcu *parent; /* recipient of SIGCHLD, wait4() reports */
struct list_head children;      /* list of my children */
struct list_head sibling;       /* linkage in my parent's children list */

「详细可以看 Linux 进程-进程数量、状态、关系」

1.3. 任务状态

1
2
3
4
5
// include\linux\sched.h

volatile long state;    /* -1 unrunnable, 0 runnable, >0 stopped */
int exit_state;
unsigned int flags;

「详细可以看 Linux 进程-进程数量、状态、关系」

1.4. 进程权限

进程权限控制是指进程能否有权限访问某个文件、能否访问其他进程、能否进行某些操作,以及进程能否被其他项目组访问。task_struct 中关于进程权限的成员变量有如下这些,其中 cred 表示我这个进程可以操作谁,实质上就是我操作别人时具有的权限是什么;real_cred 表示谁能操作我这个进程

操作其实就是一个对象对另一个对象进行某些动作。当动作要实施的时候,需要审核权限,当两边的权限匹配上了,那么就可以实施操作。

1
2
3
4
5
6
// include\linux\sched.h

/* Objective and real subjective task credentials (COW): */
const struct cred __rcu         *real_cred;
/* Effective (overridable) subjective task credentials (COW): */
const struct cred __rcu         *cred;

注释中的 Objective 是指我这个进程当前是被操作的对象,而那个想操作我的进程就是 Subjective。我能操作谁,那么这个时候我就是那个 subjective,而被我操作的那个就是 Objective。

「详细可以看 Linux 进程-权限」

1.5. 运行统计信息

task_struct 中有关于进行进程运行统计信息的字段,如下所示。主要记录了进程在用户态或者内核态上消耗的时间、上下文切换的次数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// include\linux\sched.h

u64        utime;			// 用户态消耗的CPU时间
u64        stime;			// 内核态消耗的CPU时间

unsigned long      nvcsw;	 // 自愿(voluntary)上下文切换计数
unsigned long      nivcsw;	 // 非自愿(involuntary)上下文切换计数

u64        start_time;		 // 进程启动时间,不包含睡眠时间
u64        real_start_time;	 // 进程启动时间,包含睡眠时间

「详细可以看 Linux 进程-运行统计信息」

1.6. 进程调度

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// include\linux\sched.h

//是否在运行队列上
int        on_rq;

//优先级
int        prio;
int        static_prio;
int        normal_prio;
unsigned int      rt_priority;

//调度器类
const struct sched_class  *sched_class;

//调度实体
struct sched_entity    se;
struct sched_rt_entity    rt;
struct sched_dl_entity    dl;

//调度策略
unsigned int      policy;

//可以使用哪些CPU
int        nr_cpus_allowed;
cpumask_t      cpus_allowed;
struct sched_info    sched_info;

1.7. 信号处理

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// include\linux\sched.h

/* Signal handlers: */
struct signal_struct    *signal;
struct sighand_struct    *sighand;
sigset_t      blocked;
sigset_t      real_blocked;
sigset_t      saved_sigmask;
struct sigpending    pending;
unsigned long      sas_ss_sp;
size_t        sas_ss_size;
unsigned int      sas_ss_flags;

blocked 代表被阻塞暂不处理的信号。

pending 代表尚等待处理的信号。

sighand 代表正在通过信号处理函数进行处理的信号,处理的结果是可以是忽略,也可以是结束进程。

信号处理函数默认使用的是用户态的函数栈,当然也可以开辟新的栈专门用于信号处理,这就是 sas_ss_xxx 这三个变量的作用。

「详细可以看 Linux IPC-信号」

1.8. 内存管理

进程的虚拟地址空间分为用户虚拟地址空间和内核虚拟地址空间,所以进程共享内核虚拟地址空间,但每个进程有独立的用户虚拟地址空间。这块的内容可具体查看 Linux 内存管理。

内核线程没有用户地址空间,那么 mm 将为空,active_mm 则指向此时用户态的地址空间。对于用户进程来说,mm 和 active_mm 是一样的。

1
2
3
4
// include\linux\sched.h

struct mm_struct                *mm;
struct mm_struct                *active_mm;

「详细可以看 Linux 内存管理」

1.9. 文件与文件系统

每个进程都有一个文件系统的数据结构,还有一个打开文件的数据结构。这块的内容可具体查看 Linux 文件系统。

1
2
3
4
5
6
// include\linux\sched.h

/* Filesystem information: */
struct fs_struct                *fs;
/* Open file information: */
struct files_struct             *files;

「详细可以看 Linux 文件系统」

1.10. 内核态栈

在程序的执行过程中,一旦调用了系统调用,那么就需要进入内核继续执行。跟在用户态下函数执行的过程类似,进程陷入到内核态执行时也有一个栈,我们称其为内核栈。

Linux 给每个 task 都分配了内核栈。在 x86 32 系统上,内核栈的大小是 8K。在 x86 64 系统上,内核栈的大小一般是 16K,并且要求起始地址必须是 8192 的整数倍。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// include\linux\sched.h

struct thread_info    thread_info;
void  *stack;

// arch\x86\include\asm\page_32_types.h
#define THREAD_SIZE_ORDER	1
#define THREAD_SIZE		(PAGE_SIZE << THREAD_SIZE_ORDER)

// arch\x86\include\asm\page_64_types.h
#ifdef CONFIG_KASAN
#ifdef CONFIG_KASAN_EXTRA
#define KASAN_STACK_ORDER 2
#else
#define KASAN_STACK_ORDER 1
#endif
#else
#define KASAN_STACK_ORDER 0
#endif

#define THREAD_SIZE_ORDER	(2 + KASAN_STACK_ORDER)
#define THREAD_SIZE  (PAGE_SIZE << THREAD_SIZE_ORDER)

内核栈是一个非常特殊的数据结构,它还包含了 thread_info 和 pt_regs 等数据结构,也就是说 THREAD_SIZE 的长度是指包含了 thread_info 和 pt_regs 长度之后的,如下图所示。

https://img.dawnguo.cn/Linux/image-20201103104824641.png

这段空间的最低位置,是一个 thread_info 数据结构,这个数据结构是对 task_struct 的补充。需要这个数据结构主要是因为 task_struct 通用,而 linux 需要考虑到不同体系结构,而不同体系结构会有一套自己需要保存的东西。所以往往与体系结构相关的内容都会被保存到 thread_info,也就说 thread_info 这个数据结构由不同的体系结构自己定义,可以查看 arch 目录下各体系结构对 thread_info 这个结构体的定义。

在内核代码中有一个 union,就是将 task_struct、thread_info 以及 stack 放到一起的。当然这个具体放不放在一起,得看宏定义的情况。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// include\linux\sched.h

union thread_union {
#ifndef CONFIG_ARCH_TASK_STRUCT_ON_STACK
	struct task_struct task;
#endif
#ifndef CONFIG_THREAD_INFO_IN_TASK
	struct thread_info thread_info;
#endif
	unsigned long stack[THREAD_SIZE/sizeof(long)];
};

在内核栈的最高地址端,存放的是结构体 pt_regs。这个结构体也是跟体系结构相关的,x86 32 位和 64 位的结构体的定义就是不一样的,如下所示。

在系统调用的时候,从用户态切换到内核态的时候,首先要做的第一件事情就是将用户态运行过程中的 CPU 上下文保存起来,其实主要就是保存在这个结构的寄存器变量里。这样当从内核系统调用返回的时候,就可以从进程在刚才的地方继续运行下去。而在系统调用的过程中,压栈的顺序和 struct pt_regs 中寄存器定义的顺序是一样的。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

#ifdef __i386__
struct pt_regs {
  unsigned long bx;
  unsigned long cx;
  unsigned long dx;
  unsigned long si;
  unsigned long di;
  unsigned long bp;
  unsigned long ax;
  unsigned long ds;
  unsigned long es;
  unsigned long fs;
  unsigned long gs;
  unsigned long orig_ax;
  unsigned long ip;
  unsigned long cs;
  unsigned long flags;
  unsigned long sp;
  unsigned long ss;
};
#else 
struct pt_regs {
  unsigned long r15;
  unsigned long r14;
  unsigned long r13;
  unsigned long r12;
  unsigned long bp;
  unsigned long bx;
  unsigned long r11;
  unsigned long r10;
  unsigned long r9;
  unsigned long r8;
  unsigned long ax;
  unsigned long cx;
  unsigned long dx;
  unsigned long si;
  unsigned long di;
  unsigned long orig_ax;
  unsigned long ip;
  unsigned long cs;
  unsigned long flags;
  unsigned long sp;
  unsigned long ss;
/* top of stack page */
};
#endif 

「关于用户态函数栈的或者内核态函数更多的内容,可以看 Linux 进程-函数栈」

1.11. PCB 总结

https://img.dawnguo.cn/Linux/1c91956b52574b62a4418a7c6993d8bc.jpeg

1.12. 巨人的肩膀

  1. 极客时间专栏《趣谈Linux操作系统》