Linux 操作系统:进程数据结构(task_struct)
在 Linux 中,无论进程还是线程,到了内核里面,我们统一都叫作任务(Task), 由一个统一的结构 task_struct 进行管理。 Linux 将所有的 task_struct 用链表串起来进行管理。struct list_head tasks;task_struct 的结构如图所示:任务ID任务号用于操作系统进行排期,下发任务等。在内核中,虽然进程和线程都是任务,但是还是应该加以区分,因
在 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;
参考文章:
更多推荐
所有评论(0)