梁金荣:Linux内核之话说进程

大家好,小白又来试水啦! 回望笔者之前发的技术博客,中规中矩, 阅读量更是不尽人意,看来没帮到多少人,不行,文风要改,要大改。于是乎,笔者金荣摇身一变,化作Linux OS的一个进程,我是一个进程,我来讲述我的故事。

我从哪里来

进程:“若要问我从哪里来,这还要从操作系统的发展说起。”(进程的回忆开始了)那是在20世纪50年代中期,出现了第一代计算机,那时候还需要程序员亲自手工操作穿孔的纸带卡片,虽然CPU的速度在迅速提高,但是I/O设备的速度却提高缓慢,来看看CPU和人工处理的I/O设备的日常对话。

相传,为了解决人机矛盾及CPU和I/O设备之间速度不匹配的矛盾,脱机I/O技术诞生了,看看那时候CPU在聊些什么。

为了提高系统资源利用率和吞吐量,单道批处理系统诞生了,这下好了,任务一个接一个来,CPU总算是能表现一下自己了,来看看这时候CPU和OS监督程序的对话。

虽然单道批处理系统提高了系统资源的利用率和系统的吞吐量,但是仍然不能充分地利用系统资源,因为在内存中仅有一道程序,程序在运行中发出I/O请求后,CPU就在那一直等,当时的情况是这样的。

虽然如此,但是问题依旧不大,因为在20世纪60年代中期,引入了多道程序的设计技术,形成了多道批处理系统,进一步提高了资源的利用率和系统吞吐量,我也是在这个时期开始形成的,当时可把CPU乐坏了。

后来的后来,为了方便用户,CPU继续发展,各种操作系统也是层出不穷,例如常见的Windows 、Linux、Contiki 等,然而我的作用依旧不可替代。

讲讲我的故事

你以为进程我三头六臂,真的那么大本事,可以让一个CPU核心同时运行多个程序?别你以为了,只是一种错觉,只是操作系统的并发性罢了,实际上一个CPU核心在同一时刻只能运行一个程序,只不过是我快速切换,让CPU这个高速的家伙闲不下来,知道真相的你,是不是觉得理解错了呢?事实上现在的CPU已经可以同一时刻运行多个程序啦,因为现在的CPU有多个核心呐。

我们也不容易呀,虽然让CPU这家伙完全跑了起来,有效地改善了资源利用率,提高了系统吞吐量,但是我们把操作系统弄的更复杂了,如果我们不受约束的话,就会随意争夺系统资源,给操作系统老大带来麻烦,程序也会显现出不可再现性。老大当然不允许了,于是和我约法三章,定了一些硬件同步机制、信号量机制、管程机制等一系列同步机制,好让老大更加稳定可靠。

当然我们进程之间也经常联系呢,早期的通信被人们叫做低级进程通信,当然这时候我们进程的通信人们才不知道呢。后来,OS老大更强大了,制定了高级通信机制,他给人们提供了高级通信工具,我们进程之间的通信能传送大量的数据了,对人们也透明了,这个高级通信机制,被人们归了四类,分别是:共享存储系统、管道通信系统、消息传递系统和客户机-服务器系统。

听了我们进程的故事,是不是对我有所了解呢,其实我们家族的故事多着呢,有兴趣可以自己去了解一番,这里有一张金荣制作的思维导图,有助于更好地了解我们,收好。

看看我的模样

也许你对我已经有所了解,那么现在就请你在Linux OS中看看我的真面目吧,要问我到底是谁,我只是一些程序和数据罢了,也可以认为我就是运行中的程序。Linux OS老大为了管住我,将我的状态信息、链接信息、各种标识符、进程间通信信息、时间和定时器信息、调度信息、文件系统信息、虚拟内存信息、处理器环境信息等封印在一个巨大的结构体中,江湖上称之为进程控制块(Process Contrlo Block),简称PCB,在内核源码中的定义是酱紫的,且看Linux内核3.18.43版本的源码,具体在/include/linux下的头文件sched.h中的第1237行代码。

了解我的状态,这点很重要,在一般的操作系统中,我的状态有就绪态、运行态和等待态三种状态,而在Linux OS中,为了管理上的方便,我的状态被改造成了就绪态、睡眠态、暂停态和僵死态,状态转换是酱紫的。

当然了,我们进程也并非长生不老,陈莉君老师就对我们极为了解,不信请看看陈老师语录:

随着一句fork,一个新进程呱呱落地,但这时它只是老进程的一个克隆。然后,随着exec,新进程脱胎换骨,离家独立,开始了独立工作的职业生涯。

人有生老病死,进程也一样,它可以是自然死亡,即运行到主(main)函数的最后一个”}”,从容地离我们而去;也可以是中途退场,退场有2种方式,一种是调用exit函数,一种是在主(main)函数内使用return,无论哪一种方式,它都可以留下留言,放在返回值里保留下来;甚至它还可能被谋杀,被其它进程通过另外一些方式结束它的生命。

进程死掉以后,会留下一个空壳,wait站好最后一班岗,打扫战场,使其最终归于无形。这就是进程完整的一生。

和我交朋友吧

想和我们交朋友吗,那就了解一下我的组织方式吧。为了对我们进行有效的搜索,内核建立了几个进程链表,每个进程链表由指向进程PCB的指针组成。当内核要根据我们的PID导出对应PCB的时候,遍历进程链表检查PCB中的PID太慢了,于是引入了哈希表,Linux OS中用了一个叫做pid_hashfn的宏,来实现把PID转换成表的索引的哈希函数。

在这里献上一个内核模块,功能为遍历进程链表,打印pcb相关字段的内核模块,并配上详细注释:

模块代码

# include 
# include 
# include 
# include 
# include 
# include 
# include 
# include 
//内核模块初始化函数
static int __init traverse_pcb(void)
{
    struct task_struct *task, *p;//定义指向task_struct类型的指针
    struct list_head *pos;//定义双向链表指针
    int count=0;//定义统计系统进程个数的变量
    printk("Printf process'message begin:\n");//提示模块开始运行
    task = &init_task;//指向0号进程的PCB
    
    list_for_each(pos,&task->tasks)//使用list_for_each宏来遍历进程链表
    {
        p = list_entry(pos,struct task_struct,tasks);//指向当前进程的task_struct结构
        count++;//统计系统进程个数
        printk("\n\n");//方便查看后续打印信息
        /*
        打印task_struct中的字段,其中pid:进程的pid号;state:进程的状态;
        prio:动态优先级;static_prio:静态优先级; parent'pid:父进程的pid号;
        count:文件系统信息,文件被使用的次数; umask:进程权限位的默认设置;
        使用atomic_read原子操作是为了(p->files)->count字段计数不被打断
        */
        printk("pid:%d; state:%lx; prio:%d; static_prio:%d; parent'pid:%d; count:%d; umask:%d;",    \
            p->pid,p->state,p->prio,p->static_prio,(p->parent)->pid,                              \
            atomic_read((&(p->files)->count)),(p->fs)->umask);
        //打印进程地址空间的信息
        if((p->mm)!=NULL)
            printk("total_vm:%ld;",(p->mm)->total_vm);//total_vm:线性区总的页数
    }
    printk("进程的个数:%d\n",count);//打印进程个数
    return 0;
}

//内核模块退出函数
static void __exit end_pcb(void)
{
    printk("traverse pcb is end.");
}
module_init(traverse_pcb);//入口
module_exit(end_pcb);//出口
MODULE_LICENSE("GPL");//许可证

Makefile

#产生目标文件
obj-m:=traverse_pcb.o
#路径变量,指明当前路径
CURRENT_PATH:=$(shell pwd)
#指明内核版本号
LINUX_KERNEL:=$(shell uname -r)
#指明内核源码的绝对路径
LINUX_KERNEL_PATH:=/usr/src/linux-headers-$(LINUX_KERNEL)
#编译模块
all:
    make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) modules
#清理模块
clean:
    make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) clean

编译加载

查看结果

上图中,我们可以看到每个进程的pid号,进程的状态,动态优先级,静态优先级,父进程的pid号,文件系统信息(文件被使用的次数),进程权限位的默认设置,总进程数等。有些进程的total_vm是空的,说明他们是内核线程。

故事讲到这里就结束啦,初学进程,若有不周之处,还请大家多多谅解

看完还想了解进程更多?这里给出参考资料:

[1]陈莉君,康华著.Linux 操作系统原理与应用.清华大学出版社.2012 
[2]陈莉君,张琼声,张宏伟译,Daniel P.Bovet、Marco Cesati著.深入理解LINUX内核.中国电力出版社.2008 
[3]陈莉君,康华译,RobertLove著.Linux内核设计与实现.机械工业出版社.2011 
[4]汤小丹.计算机操作系统.西安电子科技大学出版社.2014 
[5]陈莉君.Linux内核分析与应用.学堂在线视频