大家好,这里是Qyynerboomer,今天为大家讲解一下Linux基础之进程创建。
一.什么是进程:
进程就是正在运行的实例,简单来说,就是程序启动后,在内存里运行的程序的副本,主要包含以下几种元素:进程控制块,代码,数据。
二.为什么会存在进程控制块:
我们的操作系统上运行着很多的进程,必须要将他们行之有效地管理起来,操作系统于是做了以下操作:设定出进程控制块,这个进程控制块中包含着有关于进程的一切信息,并将其作为一个节点纳入名为运行队列,等待队列的链表中进行管理,这样,对于进程本身的管理(如唤醒进程,阻塞进程等等)就转换成了对于各种链表的增删查改,如果我们想让一个进程运行,操作系统就会将该进程的进程控制块纳入运行队列中进行管理,如果一个进程因为io阻塞了,那么操作系统就会将其纳入阻塞队列中进行管理。
三.如何创建进程:
在Linux系统中,我们使用fork函数来创建进程,接下来我们详细讲解一下fork()函数的功能:
fork函数是在用户态(普通用户可以进入的权限受限模式)可以调用的系统调用。底层封装了内核函数do_fork()。本质上是由这个do_fork函数来完成进程的创建。进程创建这个过程本身是在内核完成的。
do_fork函数做了以下几件事情:
1.调用alloc_task_struct()函数来获得8kb的union_thread_union内存区(在内核区);用这个东西来存储进程的进程控制块和内核栈(进程是可以在用户态和内核态之间切换的,这个东西就是用来保存进程在内核态执行的临时数据)。
2.do_fork内部定义的进程指针指向父进程的进程控制块,将其完全拷贝到子进程中。
3.进行检查:当前用户所拥有的进程数量是否超过了系统给其分配资源的限制。
4.此时子进程只是复制了父进程的信息,并没有进行本身的初始化,所以子进程的状态就会被设定为TASK_UNINTERRUPTIBLE,以保证它不会马上投入运行。
5.调用get_pid函数来分配一个有效的PID(注意这个函数是一个内核函数,只能在内核里面运行,是用来分配进程PID的,我们平常学的系统调用时getpid(),是用来查询进程的PID的)。
6.子进程的东西有些是不能和父进程一模一样的,比如子进程的父进程,子进程的兄弟进程等等,这时候就要进行更新,让子进程的信息不同于父进程。
7.所有东西准备好之后,子进程的进程控制块就被插入了内核全局进程链表和pidhash哈希表。
8.把子进程PCB的状态域设置成TASK_RUNNING,并调用wake_up_process( )把子进程插入到运行队列链表。
9.让父进程和子进程平分剩余的时间片。
10.对于父进程,函数会返回子进程的pid,而对于子进程,如果子进程创建成功,那么函数就会返回0,如果创建失败,那么函数就会返回一个负值来标识错误。
以上就是进程创建的全部过程
四.一些小疑问的讲解
可能大家已经注意到,当我们完成进程的创建的时候,会将进程同时插入两个数据结构当中,一个是链表结构,一个是哈希表结构,,一个节点怎么会同时隶属于两种结构呢?这里我们就要提到Linux一个特殊的设计,将节点作为进程控制块的一部分封装进进程控制块,这听起来十分抽象,但其实非常简单:

如图所示,进程控制块里面又封装了一个新的结构体,我们来看看这个结构体内部的结构

这个函数体内部十分简洁,仅仅包含两个指针,这看起来是不是非常熟悉,这就是我们在数据结构阶段接触到的双向链表指针结构啊,所以进程控制块内部封装了很多类似的节点,包括这样的双向链表指针节点,还有运行队列,等待队列等等,所以进程控制块可以做到隶属于多个数据结构。
那么这个pidhash哈希表有什么用?
我们都知道,链表的查询效率是比较低下的,而哈希表的查询效率是很高的,如果能知道进程的pid那么几乎是O(1)的时间复杂度我们就可以查询到我们所需要的进程控制块,那么是如何做到的呢?
首先操作系统定义了这样的一个宏,传入pid,也就是x,会根据下面的规则自动计算应该插入的下标位置
#define pid_hashfn(x) \
((((x) >> 8) ^ (x)) & (PIDHASH_SZ - 1))
之后就根据这个位置将进程控制块节点插入就好了。
可能大家会有疑惑,这不会产生冲突吗,难道不会有两个不同的pid的值,经过了上述运算之后,得到的下标是一样的吗?这样的情况当然会出现,操作系统的设计者也早就想到了这个问题,他们给出的解决方案是这个pidhash哈希表,每个元素并不是一个单纯的进程控制块节点,而是链表结构,所以每个哈希节点都是一条链表,这样的结构还有一种名字,叫做**哈希桶;**经过运算后相同的进程控制块,就插入对应的哈希桶当中,这样就解决了冲突。
以上就是关于linux进程创建的一篇简短博文,希望对大家的操作系统学习有所帮助,大家共同进步,一同进化