1. 各种进程ID

所谓进程其实就是执行中的程序而已,和静态的程序相比,进程是一个运行态的实体,拥有各种各样的资源:地址空间(未必使用全部地址空间,而是排布在地址空间上的一段段的memory mappings)、打开的文件、pending的信号、一个或者多个thread of execution,内核中数据实体(例如一个或者多个task_struct实体),内核栈(也是一个或者多个)等。针对进程,我们使用进程ID,也就是pid(process ID)。通过getpid和getppid可以获取当前进程的pid以及父进程的pid。

进程中的thread of execution被称作线程(thread),线程是进程中活跃状态的实体。一方面进程中所有的线程共享一些资源,另外一方面,线程又有自己专属的资源,例如有自己的PC值,用户栈、内核栈,有自己的hw context、调度策略等等。我们一般会说进程调度什么的,但是实际上线程才是是调度器的基本单位。对于Linux内核,线程的实现是一种特别的存在,和经典的unix都不一样。在linux中并不区分进程和线程,都是用task_struct来抽象,只不过支持多线程的进程是由一组task_struct来抽象,而这些task_struct会共享一些数据结构(例如内存描述符)。我们用thread ID来唯一标识进程中的线程,POSIX规定线程ID在所属进程中是唯一的,不过在linux kernel的实现中,thread ID是全系统唯一的,当然,考虑到可移植性,Application software不应该假设这一点。在用户空间,通过gettid函数可以获取当前线程的thread ID。对于单线程的进程,process ID和thread ID是一样的,对于支持多线程的进程,每个线程有自己的thread ID,但是所有的线程共享一个PID。

为了方便shell进行Job controll,我们需要把一组进程组织起来形成进程组。关于这方面的概念,在进程和终端文档中描述的很详细,这里就不赘述了。为了标识进程组,我们需要引入进程组ID的概念。我们一般把进程组中的第一个进程的ID作为进程组的ID,进程组中的所有进程共享一个进程组ID。在用户空间,通过setpgid、getpgid、setpgrp和getpgrp等接口函数可以访问process group ID。

经过thread ID、process ID、process group ID的层层递进,我们终于来到最顶层的ID,也就是session ID,这个ID实际上是用来标识计算机系统中的一次用户交互过程:用户登录入系统,不断的提交任务(即Job或者说是进程组)给计算机系统并观察结果,最后退出登录,销毁该session。关于session的概念,在进程和终端文档中描述的也很详细,大家可以参考那份文档,这里就不赘述了。在用户空间,我们可以通过getsid、setsid来操作session ID。

三、基础概念

1、用户空间如何看到process ID

我们用下面这个block diagram来描述用户空间和内核空间如何看待process ID的:

从用户空间来看,每一个进程都可以调用getpid来获取标识该进程的ID,我们称之PID,其类型是pid_t。因此,我们知道在用户空间可以通过一个正整数来唯一标识一个进程(我们称这个正整数为pid number)。在引入容器之后,事情稍微复杂一点,pid这个正整数只能是唯一标识容器内的进程。也就是说,如果有容器1和容器2存在于系统中,那么可以同时存在两个pid等于a的进程,分别位于容器1和容器2。当然,进程也可以不在容器里,例如进程x和进程y,它们就类似传统的linux系统中的进程。当然,你也可以认为进程x和进程y位于一个系统级别的顶层容器0,其中包括进程x和进程y以及两个容器。同样的概念,容器2中也可以嵌套一个容器,从而形成了一个container hierarchy。

容器(linux container)是一个OS级别的虚拟化方法,基本上是属于纯软件的方法来实现虚拟化,开销小,量级轻,当然也有自己的局限。linux container主要应用了内核中的cgroup和namespace隔离技术,当然这些内容不是我们这份文档关心的,我们这里主要关心pid namespace。

当一个进程运行在linux OS之上的时候,它拥有了很多的系统资源,例如pid、user ID、网络设备、协议栈、IP以及端口号、filesystem hierarchy。对于传统的linux,这些资源都是全局性的,一个进程umount了某一个文件系统挂载点,改变了自己的filesystem hierarchy视图,那么所有进程看到的文件系统目录结构都变化了(umount操作被所有进程感知到了)。有没有可能把这些资源隔离开呢?这就是namespace的概念,而PID namespace就是用来隔离pid的地址空间的。

进程是感知不到pid namespace的,它只是知道能够通过getpid获取自己的ID,并不知道自己实际上被关在一个pid namespace的牢笼。从这个角度看,用户空间是简单而幸福的,内核空间就没有这么幸运了,我们需要使用复杂的数据结构来抽象这些形成层次结构的PID。

最后顺便说一句,上面的描述是针对pid而言的,实际上,tid、pgid和sid都是一样的概念,原来直接使用这些ID就可以唯一标识一个实体,现在我们需要用(pid namespace,ID)来唯一标识一个实体。

2、内核空间如何看到process ID

虽然从用户空间看,一个pid用一个正整数表示就足够了,但是在内核空间,一个正整数肯定是不行的,我们用一个2个层次的pid namespace来描述(也就是上面图片的情形)。pid namespace 0是pid namespace 1和2的parent namespace,在pid namespace 1中的pid等于a的那进程,对应pid namespace 0中的pid等于m的那进程,也就是说,内核态实际需要两个不同namespace中的正整数来记录一个进程的ID信息。推广开来,我们可以这么描述,在一个n个level的pid namespace hieraray中,位于x level的进程需要x个正整数ID来表示该该进程。

除此之外,内核还有记录pid namespace之间的关系:谁是根,谁是叶,父子关系……

四、内核态的数据抽象

1、如何抽象pid number?

struct upid {
    int nr;
    struct pid_namespace *ns;
    struct hlist_node pid_chain;
};

虽然用户空间使用一个正整数来表示各种IDs,但是对于内核,我们需要使用(pid namespace,ID number)这样的二元组来表示,因为单纯的pid number是没有意义的,必须限定其pid namespace,只有这样,那个ID number才是唯一的。这样,upid中的nr和ns成员就比较好理解了,分别对应ID number和pid namespace。此外,当userspace传递ID number参数进入内核请求服务的时候(例如向某一个ID发送信号),我们必须需要通过ID number快速找到其对应的upid数据对象,为了应对这样的需求,内核将系统内所有的upid保存在哈希表中,pid_chain成员是哈希表中的next node。

2、如何抽象tid、pid、sid、pgid?

struct pid
{
    atomic_t count;
    unsigned int level;
    struct hlist_head tasks[PIDTYPE_MAX];
    struct rcu_head rcu;
    struct upid numbers[1];
};

虽然其名字是pid,不过实际上这个数据结构抽象了不仅仅是一个thread ID或者process ID,实际上还包括了进程组ID和session ID。由于多个task struct会共享pid(例如一个session中的所有的task struct都会指向同一个表示该session ID的struct pid数据对象),因此存在count这样的成员也就不奇怪了,表示该数据对象的引用计数。

在了解了pid namespace hierarchy之后,level成员也不难理解,任何一个系统分配的PID都是隶属于某一个namespace的,而这个namespace又是位于整个pid namespace hierarchy的某个层次上,pid->level指明了该PID所属的namespace的level。由于pid对其parent pid namespace也是可见的,因此,这个level值其实也就表示了这个pid对象在多少个pid namespace中可见。

在多少个pid namespace中可见,就会有多少个(pid namespace,pid number)对,numbers就是这样的一个数组,表示了在各个level上的pid number。tasks成员和使用该struct pid的task们关联,我们在下一节描述。

3、进程描述符中如何体现tid、pid、sid、pgid?

由于多个task共享ID(泛指上面说的四种ID),因此在设计数据结构的时候我们要考虑两种情况:

(1)从task struct快速找到对应的struct pid

(2)从struct pid能够遍历所有使用该pid的task

在这样的要求下,我们设计了一个辅助数据结构:

struct pid_link
{
    struct hlist_node node;
    struct pid *pid;
};

其中node是将task串接到struct pid的task struct链表中的节点,而pid指向具体的struct pid。这时候,我们可以在task struct中嵌入一个pid_link的数组:

struct task_struct {
……
struct pid_link pids[PIDTYPE_MAX];
……
}

Task struct中的pids成员是一个数组,分别表示该task的tid(pid)、pgid和sid。我们定义pid的类型如下:

enum pid_type
{
    PIDTYPE_PID,
    PIDTYPE_PGID,
    PIDTYPE_SID,
    PIDTYPE_MAX
};

一直以来我们都是说四种type,tid、pid、sid、pgid,为何这里少定义一种呢?其实开始版本的内核的确是定义了四种type的pid,但是后来为了节省内存,tid和pid合二为一了。OK,现在已经引入太多的数据结构,下面我们用一幅图片来描述数据结构之间的关系:

对于一个进程中的多个线程而言,每一个线程都可以通过task->pids[PIDTYPE_PID].pid找到该线程对应的表示thread ID的那个struct pid数据对象。当然,任何一个线程都有其所属的进程,也就是有表示其process id的那个struct pid数据对象。如何找到它呢?这需要一个桥梁,也就是task struct中定义的thread group 成员(task->group_leader),通过该指针,一个线程总是很容易的找到其对应的线程组leader,而线程组leader对应的pid就是该线程的process ID。因此,对于一个线程,其task->group_leader->pids[PIDTYPE_PID].pid就指向了表示其process id的那个struct pid数据对象。当然,对于线程组leader,其thread ID和process ID的struct pid数据对象是一个实体,对于非线程组leader的那些普通线程,其thread ID和process ID的struct pid数据对象指向不同的实体。

struct pid有三个链表头,如果该pid仅仅是标识一个thread ID,那么其pid链表头指向的链表中只有一个元素,就是使用该pid的task struct。如果该pid表示的是一个process ID,那么pid链表头指向的链表中多个task struct,每一个元素表示了属于该进程的线程的task struct,链表中第一个task struct是thread group leader。如果该pid并不表示一个process group ID或者session ID,那么struct pid中的pgid链表头和session链表头都是指向null。如果该pid表示一个process group ID的时候,其结构如下图所示:

对于那些multi-thread进程,内核有若干个task struct和进程对应,不过为了简单,在上面图片中,进程x 对应的task struct实际上是thread group leader对应的那个task struct。这些task struct的pgid指针(task->pids[PIDTYPE_PGID].pid)指向了该进程组对应的struct pid数据对象。而该pid中的pgid链表头串联了所有使用该pid的task struct(仅仅是串联thread group leader对应的那些task struct),而链表中的第一个节点就是进程组leader。

session pid的概念是类似的,大家可以自行了解学习。

4、如何抽象 pid namespace?

好吧,这个有点复杂,暂时TODO吧。

五、代码分析

1、如何根据一个task struct得到其对应的thread ID?

static inline struct pid *task_pid(struct task_struct *task)
{
    return task->pids[PIDTYPE_PID].pid;
}

同样的道理,我们也可以很容易得到一个task对应的pgid和sid。process ID有一点绕,我们首先要找到该task的thread group leader对应的task,其实一个线程的thread group leader对应的那个task的thread ID就是该线程的process ID。

2、如何根据一个task struct得到当前的pid namespace?

struct pid_namespace *task_active_pid_ns(struct task_struct *tsk)
{
    return ns_of_pid(task_pid(tsk));
}

这个操作可以分成两步,第一步首先找到其对应的thread ID,然后根据thread ID找到当前的pid namespace,代码如下:

static inline struct pid_namespace *ns_of_pid(struct pid *pid)
{
    struct pid_namespace *ns = NULL;
    if (pid)
        ns = pid->numbers[pid->level].ns;
    return ns;
}

一个struct pid实体是有层次的,对应了若干层次的(pid namespace,pid number)二元组,最顶层是root pid namespace,最底层(叶节点)是当前的pid namespace,pid->level表示了当前的层次,因此pid->numbers[pid->level].ns说明的就是当前的pid namespace。

3、getpid是如何实现的?

当陷入内核后,我们很容易获取当前的task struct(根据sp_svc的值),这是起点,后续的代码如下:

static inline pid_t task_tgid_vnr(struct task_struct *tsk)
{
    return pid_vnr(task_tgid(tsk));
}

通过task_tgid可以获取该task对应的thread group leader的thread ID,其实也就是process ID。此外,通过task_active_pid_ns亦可以获取当前的pid namespace,有了这两个参数,可以调用pid_nr_ns获取该task对应的pid number:

pid_t pid_nr_ns(struct pid *pid, struct pid_namespace *ns)
{
    struct upid *upid;
    pid_t nr = 0;

    if (pid && ns->level <= pid->level) {
        upid = &pid->numbers[ns->level];
        if (upid->ns == ns)
            nr = upid->nr;
    }
    return nr;
}

一个pid可以贯穿多个pid namespace,但是并非所有的pid namespace都可以检视pid,获取相应的pid number。因此,在代码的开始会进行验证,如果pid namespace的层次(ns->level)低于pid当前的pid namespace的层次,那么直接返回0。如果pid namespace的level是OK的,那么要检查该namespace是不是pid当前的那个pid namespace,如果是,直接返回对应的pid number,否则,返回0。

对于gettid和getppid这两个接口,整体的概念是和getpid类似的,不再赘述。

4、给定线程ID number的情况下,如何找对应的task struct?

这里给定的条件包括ID number、当前的pid namespace,在这样的条件下如何找到对应的task呢?我们分成两个步骤,第一个步骤是先找到对应的struct pid,代码如下:

struct pid *find_pid_ns(int nr, struct pid_namespace *ns)
{
    struct upid *pnr;

    hlist_for_each_entry_rcu(pnr,
            &pid_hash[pid_hashfn(nr, ns)], pid_chain)
        if (pnr->nr == nr && pnr->ns == ns)
            return container_of(pnr, struct pid,
                    numbers[ns->level]);

    return NULL;
}

整个系统有那么多的struct pid数据对象,每一个pid又有多个level的(pid namespace,pid number)对,通过pid number和namespace来找对应的pid是一件非常耗时的操作。此外,这样的操作是一个比较频繁的操作,一个简单的例子就是通过kill向指定进程(pid number)发送信号。正是由于操作频繁而且耗时,系统建立了一个全局的哈希链表来解决这个问题,pid_hash指向了若干(具体head的数量和内存配置有关)哈希链表头。这个哈希表用来通过一个指定pid namespace和id number,来找到对应的struct upid。一旦找了upid,那么通过container_of找到对应的struct pid数据对象。

第二步是从struct pid找到task struct,代码如下:

struct task_struct *pid_task(struct pid *pid, enum pid_type type)
{
    struct task_struct *result = NULL;
    if (pid) {
        struct hlist_node *first;
        first = rcu_dereference_check(hlist_first_rcu(&pid->tasks[type]),
                          lockdep_tasklist_lock_is_held());
        if (first)
            result = hlist_entry(first, struct task_struct, pids[(type)].node);
    }
    return result;
}

5、getpgid是如何实现的?

SYSCALL_DEFINE1(getpgid, pid_t, pid)
{
    struct task_struct *p;
    struct pid *grp;
    int retval;

    rcu_read_lock();
    if (!pid)
        grp = task_pgrp(current);
    else {
        retval = -ESRCH;
        p = find_task_by_vpid(pid);
        if (!p)
            goto out;
        grp = task_pgrp(p);
        if (!grp)
            goto out;

        retval = security_task_getpgid(p);
        if (retval)
            goto out;
    }
    retval = pid_vnr(grp);
out:
    rcu_read_unlock();
    return retval;
}

当传入的pid number等于0的时候,getpgid实际上是获取当前进程的process groud ID number,通过task_pgrp可以获取该进程的使用的表示progress group ID对应的那个pid对象。如果调用getpgid的时候给出了非0的process ID number,那么getpgid实际上是想要获取指定pid number的gpid。这时候,我们需要调用find_task_by_vpid找到该pid number对应的task struct。一旦找到task struct结构,那么很容易得到其使用的pgid(该实体是struct pid类型)。至此,无论哪一种参数情况(传入的参数pid number等于0或者非0),我们都找到了该pid number对应的struct pid数据对象(pgid)。当然,最终用户空间需要的是pgid number,因此我们需要调用pid_vnr找到该pid在当前namespace中的pgid number。

getsid的代码逻辑和getpid是类似的,不再赘述。

2. task_struct

对于内核,进程相关的调度都是通过struct task_struct来实现的,一个容量只有 64 大小的数组,数组中的元素是 task_struct 结构。

struct task_struct *task[NR_TASKS]
struct task_struct
{
/* these are hardcoded - don't touch */
    long state;            /* -1 unrunnable, 0 runnable, >0 stopped */
    long counter;
    long priority;
    long signal;
    struct sigaction sigaction[32];
    long blocked;            /* bitmap of masked signals */
/* various fields */
    int exit_code;
    unsigned long start_code, end_code, end_data, brk, start_stack;
    long pid, father, pgrp, session, leader;
    unsigned short uid, euid, suid;
    unsigned short gid, egid, sgid;
    long alarm;
    long utime, stime, cutime, cstime, start_time;
    unsigned short used_math;
/* file system info */
    int tty;            /* -1 if no tty, so it must be signed */
    unsigned short umask;
    struct m_inode *pwd;
    struct m_inode *root;
    struct m_inode *executable;
    unsigned long close_on_exec;
    struct file *filp[NR_OPEN];
/* ldt for this task 0 - zero 1 - cs 2 - ds&ss */
    struct desc_struct ldt[3];
/* tss for this task */
    struct tss_struct tss;
};

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

在这里插入图片描述

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    用于把它当前进程插入到兄弟链表中

在这里插入图片描述
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
1
2
3
4
5


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;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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指向进程运行时所使用的内存描述符, 对于普通进程而言,这两个指针变量的值相同。但是内核线程kernel thread是没有进程地址空间的,所以内核线程的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 */
};
1
2
3
4
5
6
7
这里面有个成员变量 task 指向 task_struct,所以我们常用 current_thread_info()->task 来获取 task_struct,后面章节中会详细学习。

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

Logo

更多推荐