8

从C语言源代码分析,Linux系统是如何记录和描述进程的

 3 years ago
source link: https://blog.popkx.com/%E4%BB%8Ec%E8%AF%AD%E8%A8%80%E6%BA%90%E4%BB%A3%E7%A0%81%E5%88%86%E6%9E%90-linux%E7%B3%BB%E7%BB%9F%E6%98%AF%E5%A6%82%E4%BD%95%E8%AE%B0%E5%BD%95%E5%92%8C%E6%8F%8F%E8%BF%B0%E8%BF%9B%E7%A8%8B/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

上一节简要讨论了下Linux操作系统中进程的概念,其实简单来说,进程无非就是处于运行期的程序及其相关资源的总和。这里读者应该注意“相关资源”一词,Linux 在内核中是如何记录进程的资源的呢?

Linux内核如何记录进程的资源?

首先应该明白,Linux 内核大都是采用C语言编写的,因此要弄清楚内核如何记录进程资源,只需要查看相关的C语言代码就可以了。事实上,Linux 内核是使用 task_struct 结构体描述进程的资源的,它的C语言部分代码如下,请看:

f703827194ee521d1d7b82fcb2831d3b.png

task_struct 结构体很长,在我手中的 Linux 内核C语言源代码中,它占用了280行。当然了,这其中包含很多条件编译部分,在 32 位机器上,task_struct 大约要占用 1.7 KB 的内存空间,不过考虑到它可以管理完整的进程,1.7kB 其实并不算大了。

鉴于 task_struct 结构体过长,这里不可能将其成员一一介绍清楚。如果读者和我一样好奇,粗略的浏览 task_struct 结构体,应该能够发现一些比较令人熟悉的成员,例如:

0ddaa7922f83906cd7b54588840d6bd7.png

通过C语言注释以及成员的变量名,能够看到 task_struct 结构体包含了文件系统,线程结构体,以及进程打开的文件等信息,这就与上一节文章的内容对应上了。其他成员在我之后的文章中会涉及到,这里暂不赘述。

在创建进程时,Linux 通过 slab 分配器分配 task_struct 结构,这样可以避免动态分配和释放带来的开销,提高内存的使用效率。

那么创建 task_struct 结构后,内核如何访问它呢?

根据我手上的内核C语言源代码,Linux 中还有一个结构体 thread_info,它的其中一个成员 task 指针正好适合用于索引 task_struct 结构体,在X86_64平台上,thread_info 的相关C语言代码如下,请看:

0f39291ae25b370abd633218509ac0bd.png

Linux 通常会在内核栈底或者栈顶保留 thread_info 结构,而内核栈通常大小都是可知的,因此每个进程都能方便的从自己的栈中找到 thread_info 结构,进而找到 task_struct 结构。

查找当前进程的 thread_info 结构,可以调用 current_thread_info() 函数,它的C语言代码如下,请看:

static inline struct thread_info *current_thread_info(void)
{                                 
     register unsigned long sp asm ("sp");     
     return (struct thread_info *)(sp & ~(THREAD_SIZE - 1));
}
959c247e595ec2eb0375f09262feaa25.png

可见,current_thread_info() 函数其实就是通过进程栈计算的,因此它的实现与平台架构有关,上述C语言代码其实只是 arm 平台的实现方法,其他平台的实现方法,读者可自行查阅。

此时,要获取当前进程的资源,可以通过 current_thread_info()->task 索引。

进程 PID

Linux 内核为每一个进程分配独一无二的进程标识(process identification,PID)用于区分不同的进程。PID 是一个整数,在内核的C语言源码中表示为 pid_t 类型(其实就是 int 类型)。在Linux命令行输入 ps 命令,即可查看当前进程的 PID,例如:
c957fee188dfa15cc39dbe4b93bbf9b8.png
task_struct 结构体使用成员 pid 记录进程的 PID 值,相关的C语言代码如下,请看:

3d25b5e6f4c02094eea3a5328f0d31da.png

在Linux系统中,PID 的最大值是可以调整的,早期为了兼容老版本的 Unix 和 Linux,默认最大值为 32768(short int 类型能够表示的最大值),这个值可以通过 cat 命令查看:
# cat /proc/sys/kernel/pid_max 
32768

PID 的最大值对于Linux系统的运行是有影响的,因为 PID 值是独一无二的,所以它的最大值实际上就表示系统可以同时运行的最多进程数目。对于普通的个人用户来说,32768 足够多,但是对于大型服务器来说,32768 可能就远远不够了,这时可以修改 pid_max 解决这一问题。

进程的状态

现在知道了Linux内核是如何描述和记录进程资源,以及如何区分不同进程的了。那么进程有哪些状态呢?读者应该注意到 task_struct 结构体的第一个成员 state 了,它就是用于记录进程状态的。进程的状态在C语言源代码中是使用几个宏定义的:

e19fe41fa13918a36ff8585fe470c3db.png

Linux 系统中的进程必定处于这 5 种状态之一。从上到下,分别表示进程处于:
  • 正在运行或者准备运行
  • 正在睡眠,但是可中断,接收到信号会被提前唤醒
  • 正在睡眠,并且不可中断,也即即使接收到信号也不会被唤醒
  • 被其他进程跟踪中

现在就明白有时无法通过 kill 命令杀死 D 状态的进程了,这是因为这些进程处于不响应信号的状态,kill 命令本质上是发送 SIGKILL 信号,自然无法杀死该进程。

父进程和子进程

进程的父进程和子进程也属于进程的资源,因此也被记录在 task_struct 结构体中,请看相关C语言代码:

88e8eb7c3a62779678b6c78708e4e999.png

所以要访问当前进程的父进程和子进程是方便的,例如:
struct task_struct *p = current->parent;
struct task_stuck *c  = current->children;

稍稍思考一下,应该能够发现进程结构体 task_struct 中的 parent 指针和 children 指针其实构成了一条链表,通过这样的链表,我们能够轻易的访问进程的父进程,祖父进程..., 以及子进程,孙进程... 等。不过应该明白,对于拥有大量进程的系统来说,重复遍历所有进程的开销是很大的。

本节先讨论了Linux内核如何记录和描述进程资源,可以看出,内核管理进程其实就是管理 task_struct 结构体。接着,通过C语言源代码查看了内核如何访问 task_struct 结构体,以及如何区分进程,最后我们一起还讨论了进程的状态和家族树,可见,Linux内核源代码也并不是神秘到不可理解。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK