本文正在参与 “走过Linux 三十年”话题征文活动
看到这个问题,你可能会想:
- 什么是进程?
- 进程的组成是怎样的?
- 进程有哪些特点?
- 我应该选择什么数据结构呢?
- ...
在回答题目提出的问题之前,我们需要先解决以上问题。
进程的定义
关于进程,我们可以简单的理解为:进程是静态的程序的动态的执行过程。这么说可能有点抽象,在《现代操作系统》一书中,有这么一个经典的例子。
科学家想要为他的女儿做生日蛋糕,但是他不知道怎么做,于是他需要食谱。显然巧妇难为无米之炊,更何况科学家他是个男的(手动滑稽)。所以他还需要蛋糕的原料比如面粉之类的。有了这些,科学家就可以根据食谱,用原料做出蛋糕。
假设在科学家做蛋糕的时候,电话响了,那么科学家就需要记录下他做蛋糕到了哪个步骤,然后 再去接电话,接完电话之后,科学家根据上一次记录的蛋糕做到哪的步骤,继续做蛋糕。
在这个例子中,科学家相当于CPU;食谱相当于程序;原料相当于数据;做蛋糕的过程相当于进程。
切换到接电话的过程,涉及到保存当前做蛋糕的现场,切换到另一个接电话的进程的上下文并执行,接完电话后又恢复之前做蛋糕的现场并接着执行。
进程的组成
由上面的例子,我们试想一下,让一个程序在操作系统上跑起来,需要什么?
- 首先我们的代码(食谱)是必须的吧?
- 其次经常刷题的你一定知道,程序根据不同的数据(原料)会有不同的运行结果;
- 我们的程序中可能会涉及到一些文件的读写,也就是说,进程可能还需要记录操作了哪些文件;
- 进程在内存中运行,还需要自己的地址空间;
- 假设进程在运行的时候被中断,那么需要保存现场数据以便下次加载出来继续执行;
- ……
事实上我们要让一个程序在操作系统上跑起来,需要的东西还不少,包括:
- 进程标识信息
- 处理机状态信息保存,保存进程的运行现场信息
- 进程控制信息,包括进程所用的资源(文件等),进程间通信的信息,调度信息,存储管理信息等
在现代操作系统中,进程不可能只有一个,那么显然进程就需要有自己的标识以及记录自己内容的结构,也就是进程控制块,我们常说的PCB,即process contrl block。
那么我们是不是可以定义一个结构,来描述进程呢?
在linux中,用进程描述符task_struct来表示进程,结构如下:
struct task_struct {
volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */
struct thread_info *thread_info;
struct list_head tasks;
struct list_head ptrace_children;
struct list_head ptrace_list;
struct mm_struct *mm, *active_mm;
...
以上只是一小部分task_struct 信息,事实上整个task_struct的定义就有一百多行代码
其中包括:
1.进程标识信息
包括进程标识符
pid_t pid;
pid_t tgid;
本进程的父进程和子进程
struct task_struct *real_parent; /* real parent process (when being debugged) */
struct task_struct *parent; /* parent process */
struct list_head children; /* list of my children */
struct list_head sibling; /* linkage in my parent's children list */
struct task_struct *group_leader; /* threadgroup leader */
2.进程内核栈信息
union thread_union {
struct thread_info thread_info;
unsigned long stack[INIT_THREAD_SIZE/sizeof(long)];
};
3.涉及的文件信息
/* filesystem information */
struct fs_struct *fs;
/* open file information */
struct files_struct *files;
4.信号处理
/* signal handlers */
struct signal_struct *signal;
struct sighand_struct *sighand;
sigset_t blocked, real_blocked;
struct sigpending pending;
unsigned long sas_ss_sp;
size_t sas_ss_size;
int (*notifier)(void *priv);
void *notifier_data;
sigset_t *notifier_mask;
等等。
进程的特点
现在我们已经试着用一个结构体task_struct 来表示进程,显然在操作系统中,进程不可能只有一个,那么我们怎么来组织这些进程呢?
要知道怎么组织进程,我们首先需要了解,进程有什么特点。
1.动态性
进程可以被动态的创建和结束
2.并发性
进程可以被独立调度并占用处理机运行
3.独立性
不同的进程互相之间不影响,则实际上是通过页表机制来实现的,虚拟内存机制保证了每个进程相互独立。
4.制约性
访问共享资源时或者进程间同步时会产生制约
其中,进程可以被动态的创建和结束这一特点,是不是让你想到了什么数据结构?
没错,就是链表。
进程描述符的组织
链表的一大特性在于,便于插入和删除。
但是链表也有他的缺点,假设我们想要删除某个结点,需要先拿到他的前驱结点,而拿到前驱结点,我们只能通常只能通过从头遍历的方式解决。
那么我们有没有方法改进呢,事实上,在linux中,使用双向循环链表来组织进程描述符。
双向循环链表是一种这样的组织结构。
也就是说,我们知道一个结点之后,想要获取到他的前驱结点和后继结点,都是非常容易的。
所以在linux中,使用双向循环链表task list来组织进程描述符
这有以下几个优点:
1)利于插入和删除,符合进程动态创建和结束的特点;
2)从一个进程出发可以非常容易的找到其他指定进程,这是因为双向循环链表单个结点可以 非常容易往前和往后查找的特性决定的。
事实上,在linux中,双向循环链表是一种非常重要的数据结构。
总结
曾经有人问学数据结构有什么用,我想说,假设你不知道链表是什么,那么操作系统对你而言,可能永远就只是一个概念而已。
喜欢的话就点个赞吧,你的点赞是我更文的最大动力~