1. task_struct 概述
在 Linux 内核中,无论是进程还是线程,到了内核里面,都叫做任务(Task),由统一的数据结构 task_struct 进行管理。task_struct
是 Linux 中的进程描述符,是感知进程存在的唯一实体。Linux 内核中通过一个双向循环链表将所有的 task_struct 串了起来。
不同的操作系统中,PCB 所包含的内容也会不同。
1.1. 任务 ID
// 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 中主要用以下这些成员变量来表示进程的亲缘关系:
// 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. 任务状态
// 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 表示谁能操作我这个进程。
操作其实就是一个对象对另一个对象进行某些动作。当动作要实施的时候,需要审核权限,当两边的权限匹配上了,那么就可以实施操作。
// 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 中有关于进行进程运行统计信息的字段,如下所示。主要记录了进程在用户态或者内核态上消耗的时间、上下文切换的次数。
// 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. 进程调度
// 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. 信号处理
// 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 是一样的。
// include\linux\sched.h
struct mm_struct *mm;
struct mm_struct *active_mm;
「详细可以看 Linux 内存管理」
1.9. 文件与文件系统
每个进程都有一个文件系统的数据结构,还有一个打开文件的数据结构。这块的内容可具体查看 Linux 文件系统。
// 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 的整数倍。
// 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 长度之后的,如下图所示。
这段空间的最低位置,是一个 thread_info 数据结构,这个数据结构是对 task_struct 的补充。需要这个数据结构主要是因为 task_struct 通用,而 linux 需要考虑到不同体系结构,而不同体系结构会有一套自己需要保存的东西。所以往往与体系结构相关的内容都会被保存到 thread_info,也就说 thread_info 这个数据结构由不同的体系结构自己定义,可以查看 arch 目录下各体系结构对 thread_info 这个结构体的定义。
在内核代码中有一个 union,就是将 task_struct、thread_info 以及 stack 放到一起的。当然这个具体放不放在一起,得看宏定义的情况。
// 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 中寄存器定义的顺序是一样的。
#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 总结
1.12. 巨人的肩膀
- 极客时间专栏《趣谈Linux操作系统》