如何用数据结构实现进程?

440 阅读5分钟

本文正在参与 “走过Linux 三十年”话题征文活动

看到这个问题,你可能会想:

  • 什么是进程?
  • 进程的组成是怎样的?
  • 进程有哪些特点?
  • 我应该选择什么数据结构呢?
  • ...

在回答题目提出的问题之前,我们需要先解决以上问题。 image.png

进程的定义

关于进程,我们可以简单的理解为:进程是静态的程序的动态的执行过程。这么说可能有点抽象,在《现代操作系统》一书中,有这么一个经典的例子。

科学家想要为他的女儿做生日蛋糕,但是他不知道怎么做,于是他需要食谱。显然巧妇难为无米之炊,更何况科学家他是个男的(手动滑稽)。所以他还需要蛋糕的原料比如面粉之类的。有了这些,科学家就可以根据食谱,用原料做出蛋糕。

假设在科学家做蛋糕的时候,电话响了,那么科学家就需要记录下他做蛋糕到了哪个步骤,然后 再去接电话,接完电话之后,科学家根据上一次记录的蛋糕做到哪的步骤,继续做蛋糕。

在这个例子中,科学家相当于CPU;食谱相当于程序;原料相当于数据;做蛋糕的过程相当于进程。

image.png

切换到接电话的过程,涉及到保存当前做蛋糕的现场,切换到另一个接电话的进程的上下文并执行,接完电话后又恢复之前做蛋糕的现场并接着执行。

image.png

进程的组成

由上面的例子,我们试想一下,让一个程序在操作系统上跑起来,需要什么?

  • 首先我们的代码(食谱)是必须的吧?
  • 其次经常刷题的你一定知道,程序根据不同的数据(原料)会有不同的运行结果;
  • 我们的程序中可能会涉及到一些文件的读写,也就是说,进程可能还需要记录操作了哪些文件;
  • 进程在内存中运行,还需要自己的地址空间;
  • 假设进程在运行的时候被中断,那么需要保存现场数据以便下次加载出来继续执行;
  • ……

事实上我们要让一个程序在操作系统上跑起来,需要的东西还不少,包括:

  1. 进程标识信息
  2. 处理机状态信息保存,保存进程的运行现场信息
  3. 进程控制信息,包括进程所用的资源(文件等),进程间通信的信息,调度信息,存储管理信息等

在现代操作系统中,进程不可能只有一个,那么显然进程就需要有自己的标识以及记录自己内容的结构,也就是进程控制块,我们常说的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的定义就有一百多行代码

image.png

其中包括:

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中,使用双向循环链表来组织进程描述符。

双向循环链表是一种这样的组织结构。

image.png

也就是说,我们知道一个结点之后,想要获取到他的前驱结点和后继结点,都是非常容易的。

所以在linux中,使用双向循环链表task list来组织进程描述符

image.png

这有以下几个优点:

1)利于插入和删除,符合进程动态创建和结束的特点;

2)从一个进程出发可以非常容易的找到其他指定进程,这是因为双向循环链表单个结点可以 非常容易往前和往后查找的特性决定的。

事实上,在linux中,双向循环链表是一种非常重要的数据结构。

总结

曾经有人问学数据结构有什么用,我想说,假设你不知道链表是什么,那么操作系统对你而言,可能永远就只是一个概念而已。

喜欢的话就点个赞吧,你的点赞是我更文的最大动力~