进程
基本概念
在操作系统的书本上一般这样子解释一个进程
进程(Process):是操作系统进行资源分配的最小单位。一个进程是一个程序的一次执行过程。
可是这到底是什么意思呢?
进程和程序又有什么区别呢?
下面我们先用一张图来解释进程和程序的区别
我们可以看到程序首先以文件的形式保存在磁盘当中 我们双击之后加载到了内存中 由cpu加载开始运行
这个进程被加载到内存之后我们可以看到除了原本的代码之外还多了一堆的数据
还记得我们之前的说操作系统是怎么管理的嘛? 先描述 再组织
这一堆多出来的数据就是操作系统对于进程的描述 而我们将这一堆数据称为PCB(Process Control Block) 程序控制块
那么到这里就可以回答上面的问题的
进程等于程序加上PCB
PCB 程序控制块
我们上面讲过 PCB是操作系统对于进程的描述
当我们使用ps指令的时候 我们可以看到系统中存在着大量的进程
同样的 每个进程都对应着一个PCB
我们可以这样理解
每个PCB对应着一个进程的数据 它们之间使用双链表组织起来 我们可以通过PCB来找到并管理每个进程
这样子我们就把操作系统对于进程的管理转化为了对于双链表的增删查改
task_struct是什么
进程控制块(PCB)的作用是描述进程的 而Linux系统是使用c语言写出来的
我们在c语言中一般是使用一个结构体去描述一个对象 这个在linux描述进程的结构体我们就把它叫做 task_struct
task_struct和PCB的关系就像是你和程序员 task_struct是PCB
task_struct一般会被储存在内存中
task_struct里面有什么
task_struct里面包含以下信息
- 标示符: 描述本进程的唯一标示符 用来区别其他进程
我们可以将它理解为学号
- 状态: 任务状态 退出代码 退出信号等
我们可以将这个理解为 正常上学 休学中等等
- 优先级: 相对于其他进程的优先级
- 程序计数器(pc): 程序中即将被执行的下一条指令的地址
- 内存指针: 包括程序代码和进程相关数据的指针 还有和其他进程共享的内存块的指针
- 上下文数据: 进程执行时处理器的寄存器中的数据
上下文数据的概念十分重要 下面我会使用一个小故事来帮助理解
假设你们现在学校有几个征兵名额 你被选上了 可以去当一年的兵然后回来继续学业 那你能直接过去然后一年后直接回来嘛? 显然是不可以的 如果这样子做你会发现一年后你被勒令退学了 为什么呢? 因为你事先没有给学校打招呼啊 学校以为你旷课旷了一整年 所以说你需要先向学校报备下情况之后才能去服役 这样子回来才能够继续学业 这就是上下文信息
- I/O状态信息: 包括显示的I/O请求 分配给进程的I/O设备和被进程使用的文件列表
- 记账信息: 可能包括处理器时间总和 使用的时钟总和 时间限制 记账号等
因为cpu执行进程的时候要保证尽量的公平 一个进程执行了很久了 那么就需要提高另外一个进程的优先级 那么执行多久这个信息保存在哪里呢?就是在 task_struct 中的记账信息中
- 其他信息
查看进程
我们可以通过两种方式来查看进程
- 通过系统目录查看
- 通过ps指令查看
通过系统目录查看进程
我们在根目录下可以找到一个叫做proc的目录
这个目录中含有大量的进程信息
我们可以发现 这些文件中有些是用数字表示的
而这些数字实际上就是程序的pid
如果我们想要查看这些进程的信息只需要进入里面就好了
通过ps指令查看
我们可以通过ps指令去查看进程
但是我们一般查看进程的时候使用的是这样子的语句
ps axj | head -1 && ps axj | grep 66
这段命令分为两部分
&& 连接的两个命令 如果前面执行成功了就会执行后面的语句
ps axj | head -1
这段命令的意思打出 ps axj的第一行
ps axj | grep 66
这段命令的意思是打出所有包含66的进程
获取程序的pid和ppid
- pid : pid是程序的标识符
- ppid: ppid是当前进程的父进程的标识符
我们可以写出一段代码来实验下
其中getpid() 的程序存放在 unistd.h 头文件里面
之后我们写好这段代码的makefile文件
之后使用make命令生成可执行程序
运行之后我们就可以发现该进程在循环打印子进程和父进程的pid
通过fork函数创建进程
fork是一个系统调用级别的函数 其功能就是创建一个子进程
我们可以通过如下的代码来验证它
我们的代码中只写了一行打印子进程和父进程的代码
make之后看看执行结果
我们可以发现明明只有一行代码 而这个代码却执行了两次
并且如果仔细观察我们还能发现这样子的规律
一个进程的pid是另外一个进程的ppid 这就是fork的作用
如何理解fork创建的子进程
- 从创建层面 不论是使用指令 跑代码还是使用fork创建进程 在操作系统眼中都没有区别
- 从继承层面 fork出来的子进程它本身没有代码和数据 所以说它是拷贝的父进程的代码和数据
那么代码和数据是全部拷贝过来吗?
对于代码来说 是的 但是一般创建进程之前的代码是用不到的 因为已经运行到创建进程结束了
对于数据来说父子进程的数据也就是PCB大部分是共享的 但是我们也要考虑修改的情况
因为进程相对来说具有独立性 比如说我们想让父进程返回一个10 子进程返回一个20 如果说他们完全共享一个PCB是无法做到的
这里就要用到一个写时拷贝的技术
如何让父子进程做不同的事
父子进程是共享同一段代码的 也就是说他们能做的事情是相同的
但是如果它们只能做相同的事情我们只需要使用循环语句就好了 根本没有必要大费周章来书写一个子进程
事实上我们可以通过一个叫做pid的返回值来区分父子进程 从而达到同时进行两个任务的目的
fork函数的返回值:
我们上面说过了fork是一个函数 既然是函数那么他就有返回值
而fork的返回值是这样子的
- 如果子进程创建失败返回-1
- 如果子进程创建成功 在父进程中返回子进程的pid 在子进程中返回0
那么我们就可以利用这个返回值让父子进程做不同的事情
我们可以发现这是个if else语句
执行之后我们可以发现如下的结果
我们发现竟然if else两个语句都执行了
这就是两个进程同时运行的结果
在做业务的时候我们可以将里面的代码换成业务逻辑就可以了
进程的状态
在进程从创建到消亡的这段时间会存在不同的状态 这些状态值储存在PCB当中 操作系统会根据PCB中的状态值来决定这个进程是否该运行了是否该结束了
一般来说 常见的进程状态有以下几点
在我们的linux源码中对于状态有着如下的定义
/*
* The task state array is a strange "bitmap" of
* reasons to sleep. Thus "running" is zero, and
* you can test for combinations of others with
* simple bit tests.
*/
static const char *task_state_array[] = {
"R (running)", /* 0*/
"S (sleeping)", /* 1*/
"D (disk sleep)", /* 2*/
"T (stopped)", /* 4*/
"T (tracing stop)", /* 8*/
"Z (zombie)", /* 16*/
"X (dead)" /* 32*/
};
我们可以通过ps axj来查看进程的状态
运行状态 R (run)
一个进程处于运行状态(running) 并不意味着进程一定处于运行当中,运行状态表明一个进程要么在运行中 要么在运行队列里 也就是说 可以同时存在多个R状态的进程
浅度睡眠状态 S (sleep)
一个进程处于浅度睡眠状态(sleeping) 意味着该进程正在等待某件事情的完成 处于浅度睡眠状态的进程随时可以被唤醒 也可以被杀掉
我们可以写出下面你的一段代码来验证
我们运行完之后让这个进程休眠30秒
我们可以发现该进程确实是在睡眠中的
此时我们可以使用ctrl + c 或者 kill -9 + pid 杀掉它
深度睡眠状态 D (disk sleep)
一个进程处于深度睡眠状态(disk sleep)表示该进程不会被杀掉 即便是操作系统也不行,= 只有该进程自动唤醒才可以恢复 该状态有时候也叫不可中断睡眠状态(uninterruptible sleep) 处于这个状态的进程通常会等待IO的结束
例如 某一进程要求对磁盘进行写入操作 那么在磁盘进行写入期间 该进程就处于深度睡眠状态 是不会被杀掉的 因为该进程需要等待磁盘的回复(是否写入成功)以做出相应的应答(磁盘休眠状态)
暂停状态-T (stop)
在Linux当中 我们可以通过发送SIGSTOP信号使进程进入暂停状态(stopped) 发送SIGSTOP信号可以让处于暂停状态的进程继续运行
还是用我们上面的代码举例
我们可以看到在外面发送了暂停信号了之后进程就进入了暂定状态
我们再对该进程发送SIGCONT信号 该进程就继续运行了
僵尸状态 Z(zombie)
当一个进程将要退出的时候 在系统层面 该进程曾经申请的资源并不是立即被释放 而是要暂时存储一段时间 以供操作系统或是其父进程进行读取 如果退出信息一直未被读取 则相关数据是不会被释放掉的 一个进程若是正在等待其退出信息被读取 那么我们称该进程处于僵尸状态(zombie)
僵尸状态的存在是必要的
因为如果该进程的申请的资源在其运行结束之后立即释放那么我们便不知道该进程完成的如何
要知道它完成的如何我们必须要它的调用者来接受它的返回值 在了解完毕之后才可以让这个进程终止
我们在写c语言代码的时候通常在最后加上一个返回值
int main()
{
// ..
return 0;
}
这个return的值实际上是返回给操作系统的
我们可以使用 echo $? 命令来查询上次命令的返回值
死亡状态 X (dead)
我们一般不能查询到进程的死亡状态 因为在进程死亡的那一瞬间该进程的所有资源将会被释放 该进程也不存在了 所以说我们看不到它
僵尸进程
处于僵尸状态的进程就叫做僵尸进程
我们可以人为的制造一个僵尸进程 具体操作就是让父进程一直运行 然后子进程退出 这样子的话子进程就一直无法将返回值传递给父进程 那么它就会一直处于僵尸状态
我们写出这样子的代码
之后查询子进程的pid
我们可以看到他的状态就变成了Z 僵尸状态
僵尸进程的危害
- 僵尸进程的退出状态必须一直维持下去 因为它要告诉其父进程相应的退出信息 可是父进程一直不读取 那么子进程也就一直处于僵尸状态
- 僵尸进程的退出信息被保存在task_struct(PCB)中 僵尸状态一直不退出 那么PCB就一直需要进行维护
- 若是一个父进程创建了很多子进程 但都不进行回收 那么就会造成资源浪费 因为数据结构对象本身就要占用内存
- 僵尸进程申请的资源无法进行回收 那么僵尸进程越多 实际可用的资源就越少 也就是说 僵尸进程会导致内存泄漏
孤儿进程
在Linux中的进程大多是父子关系 万一一个进程的子进程还没有结束但是他的父进程结束了 那么我们就称这个进程为孤儿进程
如果没有父进程来管理这个子进程那么他就会一直占用资源 所以说为了防止这种情况 我们设计了让1号init进程来领养这个进程
我们可以写出下面的代码来实验
我们之后可以发现
子进程的父进程变为了 1 (init进程)