第8节提到 linux 中,进程不仅是执行期的程序,它实际上是各种资源混杂的集合,那么“各种资源”究竟是指什么呢?这里不打算一句一句描述出“各种资源”,因为直接查看代码更加清楚。
用于描述 linux 进程的 task_struct 结构体
linux 内核中的进程都是用task_struct结构体描述的,进程的“各种资源”就包含在这个结构体里。借助《上一节》配置好的的 vim 阅读器,轻易就能定位出task_struct结构体的 C 语言代码:
task_struct 结构体定义在 include/linux/sched.h 里。
这个结构体相当巨大,有数 KB,查看 task_struct 结构体的各个成员,就知道 linux 的进程包含的“各种资源”包括:打开的文件,进程的地址空间,挂起的信号,进程的状态,等等。其他更多信息,可以自行查看源码。
linux 内核常常需要管理不止一个进程,所以就会有多个 task_struct 结构体需要管理,内核是通过双向循环链表管理的,如下图:
链表算是 C 语言中一种非常重要的数据类型了,我的文章还没有介绍过,后面会介绍的,敬请关注!!
task_struct结构体记录着进程的信息,所以 linux 内核会比较频繁用到这个结构体,怎样才能快速找到它呢?对于寄存器比较富足的硬件体系结构,这是相当简单的事情,分配一个寄存器记录task_struct结构体的地址就行了。但是对于 x86 这样寄存器较少的硬件体系结构,就只能另想他法了。
不仅是 x86 结构,其他寄存器较少的硬件体系结构,linux 内核都是间接计算task_struct结构体地址的。我们知道,每个进程都有自己的栈空间,不同于用户空间的进程, linux 内核进程的栈空间都是固定大小的,所以栈顶或者栈底的地址都是可知的。因此,linux 在这里创建了 thread_info 结构体,该结构体的定义如下:
struct thread_info {
struct task_struct *task; /* main task structure */
struct exec_domain *exec_domain; /* execution domain */
__u32 flags; /* low level flags */
__u32 status; /* thread synchronous flags */
__u32 cpu; /* current CPU */
int preempt_count; /* 0 => preemptable,
<0 => BUG */
mm_segment_t addr_limit;
struct restart_block restart_block;
#ifdef CONFIG_IA32_EMULATION
void __user *sysenter_return;
#endif
};
它记录着task_struct结构体的地址,所以 linux 内核可以通过进程栈计算出thread_info的地址,进而得到task_struct结构体的地址。
thread_info 结构体定义在 include/asm/thread_info.h 里。
进程的状态
我们再回到 linux 内核的task_struct结构体,它的第一个成员 state 记录了进程的当前状态,它一共只有 5 种状态,也必定是这 5 种状态之一:
- TASK_RUNNING,表示进程是可执行的,或者正在运行,或者正在运行队列里排队等待运行。
- TASK_INTERRUPTIBLE,表示进程正在睡眠,并且可能随时被唤醒信号唤醒,唤醒后,进程会被设置为 TASK_RUNNING。
- TASK_UNINTERRUPTIBLE,表示进程正在睡眠,不会被信号唤醒。
- ‘__TASK_TRACED,表示进程正在被其他进程跟踪,例如正在被 gdb 调试的进程就会是这个状态。
- ‘__TASK_STOPPED,表示进程停止执行,不能被投入运行。
在 linux 内核中,可以通过 set_task_state(task, state) 宏设置进程状态,它的定义如下:
#define set_task_state(tsk, state_value) \
set_mb((tsk)->state, (state_value))
// 继续跟踪
#define set_mb(var, value) do { var = value; barrier(); } while (0)
可以看出,这个宏其实就是简单的赋值,只是多了内存屏障用于强制其他处理器做重新排序。
set_task_state 宏位于 include/linux/sched.h 里。
进程的家族树
在 linux 中,所有进程都有着明显的“家族关系”,所有进程都有父进程,也能有自己的子进程树,拥有同一个父进程的进程们是“兄弟进程”。事实上,用于描述进程的 task_struct 结构体有一个 parent 指针,指向它的父进程,还包含一个子进程链表指针 children,用于维护它的子进程们。因此查询当前进程的父进程和子进程非常容易:
struct task_struct* parent = current->parent;
struct task_struct* children = current->children;
遍历内核的全部进程也非常简单,实际上,linux 内核提供了用于遍历进程的宏 list_for_each,它的定义如下:
#define list_for_each(pos, head) \
for (pos = (head)->next; prefetch(pos->next), pos != (head); \
pos = pos->next)
list_for_each 宏定义位于 include/linux/list.h 里。
使用该宏遍历进程的进程非常简单:
struct task_struct* task;
struct list_head* list;
list_for_each(list, ¤t->children){
task = list_entry(list, struct task_struct, sibling);
}
因为进程们的“家族关系”,依次遍历进程的父进程也非常简单:
for(task=current; task!=&init_task; task=task->parent);
因为 init_task 是系统的第一个进程,所以此处是遍历重点。