2023-06-11
原文作者:奇小葩 原文地址:https://blog.csdn.net/u012489236/category_10946851.html

Linux内核涉及到进程和程序的所有算法都围绕一个名为task_struct的数据结构建立,对于Linux内核把所有进程的进程描述符task_struct数据结构链成一个单链表(task_struct->tasks),该数据结构定义在include/sched.h中。这是系统的中主要的一个数据结构,其进程管理task_struct的结构图如下

202306111303492181.png

1 任务ID

每一个任务都应用有一个ID,作为这个任务的唯一标识。pid是进程的ID,tgid是线程的group ID,group_leader是进程组的组长。

任何一个进程,如果只有主线程,那么pid是自己,tgid也是自己,group_leader 指向的还是自己。但是如果一个进程创建了其他线程,那么就会发生变化。线程有自己的pid,tgid就是进程的主线程的pid,group_leader指向是进程的主线程。

getpid系统调用返回当前进程的TGID,而不是线程的pid,因为一个多线程应用程序中所有的线程共享相同的pid

gettid()系统调用返回线程的PID

group_leader ,通过clone创建的所有线程的task_struct的group_leader 成员,会指向组长的task_struct实例

2 亲属关系

在Linux系统中,所有进程之间都有着直接或间接地联系,每个进程都有其父进程,也可能有零个或多个子进程。拥有同一父进程的所有进程具有兄弟关系。

成员 描述
real_parent 指向其父进程,如果创建它的父进程不再存在,则指向pid为1的Init进程
parent 指向其父进程,当他终止时,必须向他的父进程发送信号,它的值通常与real_parent相同
children 表示链表的头部,链表的所有元素都是它的子进程
sibling 用于把它当前进程插入到兄弟链表中

202306111303502582.png

3 任务状态

成员 描述
state 用于记录进程的状态
exit_code、exit_signal、exit_state 用于存放进程退出值和终止信号,这样父进程就知道子进程的退出原因
flags 用于描述进程属性的一些标志位

记录进程状态

在进程的生命周期中,我们定义了各个状态,linux内核也为进程定义了5中状态,其状态如下所示

    #define TASK_RUNNING		0
    #define TASK_INTERRUPTIBLE	1
    #define TASK_UNINTERRUPTIBLE	2
    #define __TASK_STOPPED		4
    #define __TASK_TRACED		8

202306111303524223.png

  • TASK_RUNNING(可运行态或就绪态或者正在运行态):这个状态的名字是正在运行的意思,可是在Linux内核里不一定是指正在运行,所以很容易被人误解。

    它的意思是指进程处于可运行状态,或正在运行,或在就绪队列中等待运行

  • TASK_INTERRUPTIBLE(可中断睡眠状态):进程进入睡眠状态(被阻塞)来等待某些条件的达成或者某些资源的就位,一旦条件达成或者资源到位,内核就可以把这个进程的状态设置成TASK_RUNNING并加其加到就绪队列。

    这是一种浅睡眠的状态,也就是说,虽然在睡眠,等待 I/O 完成,但是这个时候一个信号来的时候,进程还是要被唤醒。只不过唤醒后,不是继续刚才的操作,而是进行信号处理。当然程序员可以根据自己的意愿,来写信号处理函数,例如收到某些信号,就放弃等待这个 I/O 操作完成,直接退出,也可也收到某些信息,继续等待。

  • TASK_UNINTERRUPTIBLE(不可中断睡眠态):进程在睡眠等待时不受干扰,不可被信号唤醒,只能死等I/O操作完成。一旦I/O操作因为特殊原因不能完成,这个时候,谁也叫不醒这个进程。

    通过ps命令看到的被标记为D状态的进程,就属于不可中断态的进程,不可以发送SIGKILL信号使它终止,也有人把这个状态称为深度睡眠状态

  • TASK_STOPPED(终止态):在进程接收到 SIGSTOP、SIGTTIN、SIGTSTP 或者 SIGTTOU 信号之后进入该状态。

  • EXIT_ZOMBIE(僵尸态):进程已经消亡,但是task_struct数据结构还没有释放,每个进程在它的声明周期中都要经历这几个状态,子进程退出时,父进程可以通过wait()或者waitpid()来获取子进程的消亡原因。

4. 进程权限

unix是诞生于20世纪70年代的分时多任务多用户操作系统,当时的场景是许多用户同时登陆到一台主机,运行多个各自的进程。

linux使用一个整数来标记运行进程的用户,这个整数倍称为user id,简称uid。人通常被分组,比如这几个人在研发工作,被分到研发组,那几个人做销售工作,被分配到销售组。linux用另外一个整数来标记用户组,这个被称为group id,简称gid。

uid和gid是操作系统自主访问控制的基础。

成员 描述
real_cred 说明谁能操作我这个进程
cred 说明这个进程能够操作谁
    struct cred {
    ......
            kuid_t          uid;            /* real UID of the task */
            kgid_t          gid;            /* real GID of the task */
            kuid_t          suid;           /* saved UID of the task */
            kgid_t          sgid;           /* saved GID of the task */
            kuid_t          euid;           /* effective UID of the task */
            kgid_t          egid;           /* effective GID of the task */
            kuid_t          fsuid;          /* UID for VFS ops */
            kgid_t          fsgid;          /* GID for VFS ops */
    ......
            kernel_cap_t    cap_inheritable; /* caps our children can inherit */
            kernel_cap_t    cap_permitted;  /* caps we're permitted */
            kernel_cap_t    cap_effective;  /* caps we can actually use */
            kernel_cap_t    cap_bset;       /* capability bounding set */
            kernel_cap_t    cap_ambient;    /* Ambient capability set */
    ......
    } __randomize_layout;

5 运行统计

在进程的运行过程中,会有一些统计量,具体你可以看下面的列表。这里面有进程在用户态和内核态消耗的时间、上下文切换的次数等等。

成员 描述
utime 用户态消耗的CPU时间
stime 内核态消耗的CPU时间
nvcsw 自愿上下文切换计数
nivcsw 非资源上下文切换计数
start_time 进程启动时间,不包括睡眠时间
real_start_time 进程启动时间,包括睡眠时间

6 调度相关

进程的状态切换往往涉及调度,下面这些字段都是用于调度的。本次只是进行概要说明,后面进程调度会详细分析

成员 描述
on_rq 是否在运行队列上
prio 用于保存动态优先级
static_prio 用于保存静态优先级,可以通过nice系统调用来进行修改
normal_prio 取决于静态优先级和调度策略
sched_class 调度类
se 普通进程的调用实体,每个进程都有其中之一的实体
policy 调度策略

7 信号处理

主要定义了哪些信号被阻塞暂不处理(blocked),哪些信号尚等待处理(pending),哪些信号正在通过信号处理函数进行处理(sighand)。处理的结果可以是忽略,可以是结束进程等等。

成员 描述
signal 指向进程的信号描述符
sighand 指向进程的信号处理程序描述符
blocked 表示被阻塞信号的掩码,real_blocked表示临时掩码
pending 存放私有挂起信号的数据结构

8 内存管理

每个进程都有自己独立的虚拟内存空间,这需要有一个数据结构来表示,就是 mm_struct,之前在内存管理章节中有学习,详细见linux内存管理笔记(三十)----进程虚拟地址

成员 描述
mm 程所拥有的用户空间内存描述符,内核线程无的mm为NULL
active_mm active_mm指向进程运行时所使用的内存描述符,对于普通进程而言,这两个指针变量的值相同。但是内核线程kernelthread是没有进程地址空间的,所以内核线程的tsk->mm域是空(NULL)。但是内核必须知道用户空间包含了什么,因此它的active_mm成员被初始化为前一个运行进程的active_mm值。

9 文件与文件系统

每个进程有一个文件系统的数据结构,还有一个打开文件的数据结构。这个我们放到文件系统那一节详细讲述。

成员 描述
fs 文件系统的数据结构
files 表示进程当前打开的文件数据结构

10 内核栈

在程序在程序执行过程中,一旦调用到系统调用,就需要进入内核继续执行。那如何将用户态的执行和内核态的执行串起来呢?这就需要以下两个重要的成员变量:

成员 描述
thread_info 是task_struct的补充,存储于体系结构有关的内容

那如果一个当前在某个 CPU 上执行的进程,想知道自己的 task_struct 在哪里,又该怎么办呢?

    struct thread_info {
    	unsigned long		flags;		/* low level flags */
    	mm_segment_t		addr_limit;	/* address limit */
    	struct task_struct	*task;		/* main task structure */
    	int			preempt_count;	/* 0 => preemptable, <0 => bug */
    	int			cpu;		/* cpu */
    };

这里面有个成员变量 task 指向 task_struct,所以我们常用 current_thread_info()->task 来获取 task_struct,后面章节中会详细学习。

11 总结

通过这一节的学习,你会发现,一个进程的运行竟然要保存这么多信息,这些信息都可以通过命令行取出来,后面我们单独章节来学习怎么查看这些信息。

12 参考

趣谈Linux操作系统

阅读全文