前言
要讲进程你就不能只跟着教科书上讲他的概念
从描述进程的结构体——PCB,到地址空间
从进程id到进程状态,从状态到进程切换,从切换到进程调度,再到并行并发,和进程独立性。
一、什么是进程
教材观点:进程就是加载到内存的程序和正在运行的程序,并且进程可以排队
而是要讲从 先描述,再组织 这一总要的组织方法开始讲起。
所有的进程,在加载到内存之前,都会先生成一个PCB结构体加载到内存中。即先有 描述进程的结构体,再有进程。 所以我们就从 进程的 PCB结构体开始
1、什么是PCB,PCB里有什么
(1)什么是PCB
进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合。
这个进程控制块实际上就是一个结构体,名字为PCB,Linux操作系统下的PCB是: task_struct。即为描述进程的各种信息的结构体。
task_struct是Linux内核的一种数据结构,它会被装载到RAM(内存)里并且包含着进程的信息。
(2)PCB 里有什么内容
PCB是操作系统课程里,对描述进程的结构体的统称,在LInux系统的源代码中即为task_struct。
其中这个结构体里面会有以下的内容
- 标示符: 描述本进程的唯一标示符,用来区别其他进程。
- 状态: 任务状态,退出代码,退出信号等。
- 优先级: 相对于其他进程的优先级。
- 程序计数器: 程序中即将被执行的下一条指令的地址。
- 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
- 上下文数据: 进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器]。
- I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
- 记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
- 其他信息……
上面讲了那么多有关进程的知识点,那下面带大家看一看实际中的进程信息吧
(3)查看进程
进程的信息可以通过 /proc 系统文件夹查看
如:要获取PID为1的进程信息,你需要查看 /proc/1 这个文件夹。
2、进程的PID
我们知道,无论是一个指令 还是 一个代码经过编译运行后,都会形成一个进程,上面也讲了进程在跑起来之前会先生成并加载包含其属性的结构体到内存中,那进程真正运行起来 有哪些属性是我们能够看到的呢?
用 ps aux 指令就可以查到当前所有在运行的指令 再用管道 | 连接上过滤命令 grep test 找到我们用下面的代码写的程序跑起来的进程 grep -v grep 是用于过滤掉本条指令生成的进程
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
while(1){
sleep(1);
}
return 0;
}
从上面的截图我们可以看到,有很多的信息,当然现在都还看不懂,我们可以一点一点了解学习。
首先最左边的 root 就是执行这个程序的用户名 3239 就是正是现在要讲的 进程 id 。其他暂时就不管了。
其实除了这种方式可以获取到一个进程的id 系统也为我们提供了调用接口 getpid() 和 getppid(), 分别是获取 本进程的id和父进程的id
这里预言一下 所有的进程都有父进程吗
当然我们可以 用 man getpid 来查看这个接口的详细信息 如参数,返回值
用代码使用一下
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
printf("pid: %d\n", getpid());
printf("ppid: %d\n", getppid());
return 0;
}
执行结果 这便是每一个正在运行的进程的主键——唯一标识符
二、创建进程
一般的进程,比如打开一个软件,运行一个代码文件,执行一句指令等等,都可以生成一个进程,那创建一个进程和生成一个进程有什么区别吗,值得我专门写一个大点来讲解它。
当然,生成一个进程固然容易,可是这只是使用它,无论是生成进程前的PCB结构体,将其加载进内存中,还是我们打字输入一个指令就可以执行,这些都是操作系统帮我们完成的,我们学习的正是操作系统是怎么完成这些任务的,其背后的原理是什么。所以我们要在代码中调用 创造进程 的系统调用接口,(如果不知道什么是系统调用接口,系统调用接口 和 函数调用有什么区别的可以看一下我的 操作系统如何实现软硬件资源管理 那篇博客,这里这个概念我就直接用了)来自己创建出进程。而fork()就是创建子进程的系统调用接口。
前面学的 用getpid()和getppid()来获取 进程的pid 和 其父进程的ppid , 可以用来验证 fork 函数是否能完成它的任务 —— 分流 生成子函数
man fork 用这个指令就能 获取 fork 的具体内容
接下来我们就用一段代码来使用一下这个fork函数吧
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
14 int main()
15 {
16 printf("before fork: I am a prcess, pid: %d, ppid: %d\n", getpid(), getppid());
17 sleep(5);
18 printf("开始创建进程啦!\n");
19 sleep(1);
20 pid_t id = fork(); //fork()创建子进程
21 if(id < 0) return 1;
22 else if(id == 0){ // 子进程
23 while(1){
24 printf("after fork, 我是子进程: I am a prcess, pid: %d, ppid: %d, return id: %d\n", getpid(), getppid(), id);
25 sleep(1);
26 }
27 }else{ // 父进程
28 while(1){
29 printf("after fork, 我是父进程: I am a prcess, pid: %d, ppid: %d, return id: %d\n", getpid(), getppid(), id);
30 sleep(1);
31 }
32 }
33 sleep(2);
34 return 0;
35 }
~
可是结果显示 else if 和 else 里面的都打印出来了 难道 id 这一个变量,能既大于零又小于零?
这里就简单讲一下,fork的作用吧,大家感兴趣就在评论区留言,我会一一解答,如果有时间,就写一个专讲fork的博客。
- fork返回值:
给父进程,返回子进程的pid,给子进程返回0。原因是父进程与子进程是一对多的关系,父:子 = 1 :n 子找父,直接用getppid()接口就可以找到,而父找子,只能通过其返回值。
- fork函数为什么会返回两次
因为父子两进程,是两个不同的执行流,故而会返回两次
- 同一个变量,怎么可能既大于0,又等于0
首先:任意两个进程在运行时是互不影响的,但fork创建的子进程,是共享的父进程的代码,若是子进程改了一个变量的值,就会影响到父进程,所以操作系统是不能够让子进程直接修改代码的,会通过一些写时拷贝的技术手段来使得进程间是独立运行的,即子进程的PCB结构体中的代码指针,是指向父进程的代码,但若是子进程想要修改什么的时候,操作系统就会触发写时拷贝,先拷一份给给子进程,再允许它修改。 所以,id存的返回值先是直接存到了父进程的id里,但子进程在返回时,发生了写时拷贝。而父子进程在使用id的时候,就是用的各自的id,所以 才会出现,else if 和 if都成立的现状。
三、 进程状态
1、为什么会有进程状态的概念
为了弄明白正在运行的进程是什么意思,我们需要知道进程的不同状态。一个进程可以有几个状态(在 Linux内核里,进程有时候也叫做任务)。
2、进程有哪些状态
- R运行状态(running): 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里。
- S睡眠状态(sleeping): 意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠
- D磁盘休眠状态(Disk sleep)有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。
- T停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。
- X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态。
其实进程的状态无非就是三大类:运行,阻塞,挂起
- 运行状态:正在运行,或是在运行队列里
- 阻塞状态:当我们的进程在资源没有就绪,我们的进程task_struct将设置自己为阻塞状态,且将自己的PCB列入等待资源提供的队列里。
- 挂起状态:其中最常见的就是----阻塞挂起:临时腾空间,临时阻塞的进程在内存不足时,可能导致所有的进程崩溃,操作系统就会把阻塞的进程写入外设磁盘的特定分区中,要用的时候再唤起写入内存。
3、如何查看进程状态
ps aux ——查看进程状态的指令
循环查看进程的指令:
while : ; do ps ajx | head -1 && ps ajx | grep a.out | grep -v grep ; sleep 1 ; done
运行状态 R
由于CPU在时间片的轮转下,运行一个进程速度很快,所以R状态很难看得到,但是root是一个正在运行的用户,会一直占用着CPU,则为R状态
睡眠状态 S
更多的时候,进程都是处在S状态下,如下面的 父进程 所处的状态
僵尸状态 Z
今天就先肝到这里,后面的进程切换,进程调度,进程独立性 在 进程下篇 进行讲解咯
在下主页还有文件管理系统和硬件管理系统及其之间的联系,关注小O,带你学习三系统之间的联系