在 Linux 中,无论进程还是线程,到了内核里面,我们统一都叫作任务(Task), 由一个统一的结构 task_struct 进行管理。 Linux 将所有的 task_struct 用链表串起来进行管理。

struct list_head tasks;

task_struct 的结构如图所示:
在这里插入图片描述

任务ID

任务号用于操作系统进行排期,下发任务等。在内核中,虽然进程和线程都是任务,但是还是应该加以区分,因为任务下发和展示是区分进程级和线程级的,所以 task_struct 中有两个任务号, pid 是 process ID, tgid 是 thread group ID。

任何一个进程,如果只有主线程,那 pid 是自己, tgid也是自己, group_leader 指向的还是自己。

如果一个进程创建了其它线程,线程有自己的 pid, tgid 就是进程的主线程的 pid, group_leader 指向的就是进程的主线程。

通过比较 pid 于 tgid 就可以知道 task_struct 是代表一个进程还是一个线程了。同时通过指针能够快速定位到数据位置。

信号处理

task_struct中关于信号处理的字段:


/* 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)。处理的结果可以是忽略,也可以是结束任务。

下发信号任务是分进程和线程的,task_struct 中struct sigpending pending 和struct signal_struct *signal 中的 struct sigpending shared_pending ,用于区分信号是本任务还是线程组共享的。

任务状态

task_struct 里面,任务状态有以下几个变量:


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

state(状态)取值定义在 Linux 源文件 include/linux/sched.h 头文件中

/* Used in tsk->state: */
#define TASK_RUNNING			0x0000	0
#define TASK_INTERRUPTIBLE		0x0001	1
#define TASK_UNINTERRUPTIBLE	0x0002	2
#define __TASK_STOPPED			0x0004	4
#define __TASK_TRACED			0x0008	8
/* Used in tsk->exit_state: */
#define EXIT_DEAD			0x0010	16
#define EXIT_ZOMBIE			0x0020	32
#define EXIT_TRACE			(EXIT_ZOMBIE | EXIT_DEAD)
/* Used in tsk->state again: */
#define TASK_PARKED			0x0040	64
#define TASK_DEAD			0x0080	128
#define TASK_WAKEKILL		0x0100	256
#define TASK_WAKING			0x0200	512
#define TASK_NOLOAD			0x0400	1024
#define TASK_NEW			0x0800	2048
#define TASK_STATE_MAX		0x1000	4096

从定义看,state 是通过 bitset 的方式进行设置的,也就是说,当前是什么状态, 哪一位就设置为一。
在这里插入图片描述
TASK_RUNNING 表示进程在时刻准备运行的状态。

运行中的进程,一旦要进行一些 I/O 操作, 需要等待 I/O 完毕,这个时候会释放 CPU, 进入睡眠状态,Linux 中有两种睡眠状态:
TASK_INTERRUPTIBLE, 可中断的睡眠状态,在等待 I/O 完成过程中,这个时候一个信号来的时候,进程被唤醒,进行信号处理。
TASK_UNINTERRUPTIBLE, 不可中断的睡眠状态。不可被信号唤醒,只能死等 I/O 操作完成, 一旦 I/O 无法完成,这个时候,进程无法被唤醒, 使用 kill 信号也不能唤醒,只能重启电脑。

另外还有一种新的进程睡眠状态, TASK_KILLABLE, 可以终止的新睡眠状态。它的运行原理类似 TASK_UNINTERRUPTIBLE, 只不过可以响应致命信号。实际上是设置了两位。

#define TASK_KILLABLE        (TASK_WAKEKILL | TASK_UNINTERRUPTIBLE)

TASK_STOPPED 是进程收到 SIGSTOP、SIGTTIN、SIGTTOU信号之后进入的状态。

TASK_TRACED 表示进行被 debugger 等进程监视, 进程执行被调试程序所停止。当一个进程被另外的进程所监视,每一个信号都会让进程进入该状态。

一个进程要结束,先进入的是 EXIT_ZOMBIE状态,这个时候它的父进程还没有使用 wait() 等系统调用来获知它的终止信息,此时进程就成了僵尸进程。

EXIT_DEAD 是进程的最终状态。

进程状态和进程的运行、调度有关系,还有其它的一些状态,我们称为标志。放在 flags 字段中, 这些字段都被定义成宏,以 PF 开头。这里有几个例子


#define PF_EXITING    0x00000004
#define PF_VCPU      0x00000010
#define PF_FORKNOEXEC    0x00000040

PF_EXITING 表示正在退出。当有这个标志时,在函数 find_alive_thread 中,找还活着的线程时,遇到这个 flag, 就直接跳过

PF_VCPU 表示进程运行在虚拟 CPU 上。 在函数 account_system_time 中, 统计进程的系统运行时间,如果有这个 flag , 就调用 account_guest_time,按照客户机的时间进行统计。

PF_FORKNOEXEC 表示 fork 完了,还没有 exex。 在 _do_fork 函数调用 copy_process, 这个时候把 flag 设置为 PF_FORKNOEXEC。当 exec 中调用了 load_elf_binary 的时候,又把这个 flag 去掉。

进程调度

下面的字段是关于进程调度的:


//是否在运行队列上
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;

运行统计信息

统计进程在用户态和内核态消耗的时间、上下文切换的次数等等。


u64        utime;//用户态消耗的CPU时间
u64        stime;//内核态消耗的CPU时间
unsigned long      nvcsw;//自愿(voluntary)上下文切换计数
unsigned long      nivcsw;//非自愿(involuntary)上下文切换计数
u64        start_time;//进程启动时间,不包含睡眠时间
u64        real_start_time;//进程启动时间,包含睡眠时间

进程亲缘关系

任何一个进程都是从父进程 fork 来的,任何一个进程都有父进程。所以,整个进程就是一颗进程树。


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 */
  • parent 指向父进程。当它终止时, 必须向它的父进程发送信号。一般 real_parent 和parent是一样的,但也有另外的情况
  • children 表示表链的头部。链表中的所有元素都是它的子进程
  • sibling 用于把当前进程插入到兄弟链表中

关系如图所示:在这里插入图片描述

进程权限

用于控制进程能否访问某些文件、某些进程以及本进程能否被其它进程访问。

进程权限定义如下:


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

real_cred 说明谁能操作我这个进程, 而 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;

uid 和 gid 表示 real user/group id。表示用户和所属组信息。表明那个用户/用户组的进程启动我。

euid 和 egid, 表示 effective user/group id。 当这个进程要操作消息队列、共享内存、信号量等对象的时候,就是在比较这个用户和组是否有权限。

fsuid 和 fsgid, 表示 filesystem user/group id。 这个是文件操作时会审核的权限。

一般来说, fsuid、 euid 和 uid 是一样的, fsgid、egid 和 gid 也是一样的。因为谁启动的进程,就应该审核启动的用户到底有没有这个权限。
但是也有特殊的情况。
在这里插入图片描述
例如,用户 A 想玩一个游戏,这个游戏的程序是用户 B 安装的。游戏这个程序文件的权限为 rwxr–r–。A 是没有权限运行这个程序的,所以用户 B 要给用户 A 权限才行。用户 B 说没问题,都是朋友嘛,于是用户 B 就给这个程序设定了所有的用户都能执行的权限 rwxr-xr-x,说兄弟你玩吧。

于是,用户 A 就获得了运行这个游戏的权限。当游戏运行起来之后,游戏进程的 uid、euid、fsuid 都是用户 A。看起来没有问题,玩得很开心。

用户 A 好不容易通过一关,想保留通关数据的时候,发现坏了,这个游戏的玩家数据是保存在另一个文件里面的。这个文件权限 rw-------,只给用户 B 开了写入权限,而游戏进程的 euid 和 fsuid 都是用户 A,当然写不进去了。

那怎么解决这个问题呢?我们可以通过 chmod u+s program 命令,给这个游戏程序设置 set-user-ID 的标识位,把游戏的权限变成 rwsr-xr-x。这个时候,用户 A 再启动这个游戏的时候,创建的进程 uid 当然还是用户 A,但是 euid 和 fsuid 就不是用户 A 了,因为看到了 set-user-id 标识,就改为文件的所有者的 ID,也就是说,euid 和 fsuid 都改成用户 B 了,这样就能够将通关结果保存下来。

在 Linux 里面,一个进程可以随时通过 setuid 设置用户 ID,所以,游戏程序的用户 B 的 ID 还会保存在一个地方,这就是 suid 和 sgid,也就是 saved uid 和 save gid。这样就可以很方便地使用 setuid,通过设置 uid 或者 suid 来改变权限。

除了以用户和用户组控制权限, Linux 还有一个机制就是 capabilities。用户控制普通用户进程所拥有的权限,当有这个权限的时候,才能做这些操作。用位图的形式表示权限, 在 capalibity.h 可以找到定义的权限。


#define CAP_CHOWN            0
#define CAP_KILL             5
#define CAP_NET_BIND_SERVICE 10
#define CAP_NET_RAW          13
#define CAP_SYS_MODULE       16
#define CAP_SYS_RAWIO        17
#define CAP_SYS_BOOT         22
#define CAP_SYS_TIME         25
#define CAP_AUDIT_READ          37
#define CAP_LAST_CAP         CAP_AUDIT_READ

cap_permitted 表示进程能够使用的权限。但是真正其作用的是 cap_effective。 cap_permitted 中可以包含 cap_effective 中没有的权限。一个进程可以在必要的时候,放弃自己的某些权限,这样更加安全。如果代码因为漏洞被攻破,进程没有权限会啥也干不了,就无法进一步攻破。

cap_inheritable 表示当可执行文件的扩展属性设置了 inheritable 位时,调用 exec 执行该程序会继承调用者的 inheritable 集合,并将其加入到 permitted 集合。但在非 root 用户下执行 exec 时,通常不会保留 inheritable 集合,但是往往又是非 root 用户,才想保留权限,所以非常鸡肋。

cap_bset, 也就是 capability bounding set , 是系统中所有进程允许保留的权限。如果这个集合中不存在某个权限,那么系统中的所有进程都没有这个权限。即使以超级用户权限执行的进程,也是一样的。这样做是有好处的,例如,系统启动以后,将加载内核模块的权限去掉,所有的进程都将不能加载内核模块。这样,即便这台机器被攻破,也做不了太多有害的事情。

cap_ambient 是较新加入内核的,就是为了解决 cap_inheritable 鸡肋的状况,也就是, 非 root 用户进程使用 exec 执行一个程序的时候,如何保留权限的问题。当执行 exec 的时候, cap_ambient 会被添加到 cap_permitted 中, 同时设置到 cap_effective 中。

内存管理

每个进程都有自己独立的虚拟内存空间,这需要有一个数据结构来表示,就是 mm_struct。


struct mm_struct                *mm;
struct mm_struct                *active_mm;

文件与文件系统

每个进程有一个文件系统的数据结构, 还有一个打开文件的数据结构。


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

参考文章:

获取进程信息相关命令

趣谈 Linux 操作系统

Logo

更多推荐