一、windows和linux中的进程
一个已经加载到内存的程序,叫做进程
进程也叫做任务
比如在windows中,这个任务管理器就可以看到进程
而在linux中,我们使用
ps axj
就可以看到我们的进程了
二、先描述
我们先使用如下代码
如下所示,就可以查看到进程了
在这个过程中,我们的这个可执行程序就会从磁盘中加载到内存中,然后经过CPU,最终输出到显示器上。
这个加载到内存中的程序就是一个进程
像我们电脑的操作系统开机的时候也一样,开机的时候,就是操作系统加载到内存的时候。这也是进程
这些加载到内存,其实就是将这些二进制数据(代码和一些数据)从磁盘中搬到内存中
如下图所示,当一个程序要运行的时候,他需要先将这些二进制文件从磁盘中搬到操作系统中。
但是常识告诉我们,一个操作系统,不仅仅只能运行一个进程,可以同时运行多个进程
所以说,操作系统必须将这些进程给管理起来
那么应该如何管理进程呢?
当然还是先描述,在组织
所以任何一个进程,在加载到内存的时候,形成真正的进程时,操作系统要先创建描述进程属性的结构体对象----PCB(process ctrl block 进程控制块)
这就是类似于人是怎么样辨别认识一个事情或者对象的
当然是都是通过属性认识的。
当属性够多,这一堆属性的集合,就是目标对象
这个PCB就是进程属性的集合
而在C语言中,我们可以用struct结构体来描述这个集合。
这个结构体里面就有进程编号、进程的状态、优先级、相关的指针信息…等等信息
然后根据进程的PCB属性,就可以为该进程创建对应的PCB对象了
不过我们的操作系统除了创建一个PCB对象之外,还要去将代码和数据加载到内存中
就好比我们现在是一个学生,那么PCB就是我们的档案,代码和数据就是我们本人。只有当这两部分都在学校的时候,我们才是这个学校的学生。
所以描述进程的PCB的结构体和该进程的代码和数据合起来才称作进程
所以所谓的进程 = 内核PCB数据结构对象 + 你自己的代码和数据
内核PCB数据结构对象是描述你这个进程的的所有的属性值
所以操作系统要做管理的时候,不需要对我们自己做出管理,只需要对我们这个内核PCB数据结构对象做出管理即可
这个PCB对象里面是有相关的指针信息的,所以可以通过这个PCB对象直接找到我们的
上面这个过程就是一个进程的描述的过程,即先描述
三、在组织
可是我们操作系统经常要持续很多个进程的。所以这就需要组织起来了,从而达到对我们的进程做出管理
为了能够将这些数据组织起来,所以我们可以在每一个PCB对象中加上一个相应的指针,用来找到下一个PCB对象,即如同链表的结构
所以在操作系统中,对于进程的管理,就变成了对于这个单链表的增删查改了
这个PCB就好比我们每个人的简历一样,在我们找工作的时候,HR都是直接对我们的简历做出管理的。当我们投递简历以后,我们会看到在排队中,这个排队就指的是这个PCB在排队。而不是我们本人在排队
四、具体的Linux系统是如何做的?
1.基本概念
课本概念:程序的一个执行实例,正在执行的程序等
内核观点:担当分配系统资源(CPU时间,内存)的实体
2.描述进程-PCB
进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合。
课本上称之为PCB(process control block),Linux操作系统下的PCB是: task_struct
3.task_struct和PCB的关系
- task_struct 是PCB具体的一种
- 在Linux中描述进程的结构体叫做task_struct。
- task_struct是Linux内核的一种数据结构类型(自定义类型),它会被装载到RAM(内存)里并且包含着进程的信息
4.task_struct内容分类
- 标示符: 描述本进程的唯一标示符,用来区别其他进程。
- 状态: 任务状态,退出代码,退出信号等。
- 优先级: 相对于其他进程的优先级。
- 程序计数器: 程序中即将被执行的下一条指令的地址。
- 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
- 上下文数据: 进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器]。
- I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
- 记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
- 其他信息
5.linux具体如何做的?
在linux中PCB 就是task_struct 结构体,里面包含进程的所有属性
Linux中如何组织进程呢?,linux内核中,最基本的组织进程task_struct的方式,是采用双向链表的
不过要注意的是,有可能这个task_struct既是属于一个双链表,又是属于一个队列的。他是比较复杂的
6.查看进程
我们可以使用这个命令查看进程
top
同时我们在前面所演示这个命令也是一个查看进程的命令
这里这里的&&代表的十左侧的要执行成功,右侧的也要执行成功。即两条都要执行
ps ajx | head -1 && ps ajx | grep myprocess
除了使用&&,也可以使用;来隔开
ps ajx | head -1 ; ps ajx | grep myprocess
如下所示,我们可以看到两个进程了
前面两个是这个可执行程序由于跑了两份,所以的两份进程。
下面的第三个进程是grep本身的进程,因为它本身也带有myprocess,所以就把它自己的进程过滤出来了。而之前的那些指令的进程早就已经结束了,所以不会显示出来。这里是因为正好grep执行的时候把他自己给过滤出来了
注意看
注意这个ID值,虽然这两个程序是一样的,但是他们的ID是不一样的,虽然将同一个程序执行了两遍,但是它还是两个进程,因为他们创建了两个不同的PCB
除了上面这种方法还有这样一种方法
ls /proc
这个是linux系统中比较重要的,也比较奇怪的一个目录
它在关机的时候数据就全没了,在开机的时候就又会创建这个目录文件
这个其实因为因为操作系统用文件系统的方式把内存当中的文件,包括进程全部可视化出来了
它这上面的数据都是内存级的
我们可以来证明一下
我们先关掉一个进程
当我们再次使用这个
ls /proc
这个指令的时候
对于这些蓝色的数字,我们知道它一定是目录,而对于黑色的我们先不管
而这些蓝色的数字就是当前进程的PID,所以在创建进程的时候,操作系统会创建一个与PID一样名字的文件夹,这个目录里面保存这个进程的大部分属性
我们可以通过这个找到我们这个进程的目录
然后我们可以查看一下这里面的内容
如果我们将我们这个进程之间结束掉,那么我们这里就用这种方法找不到这个进程了,这个目录就自动删除了
如果我们重启一个进程,那么就又出来了
不过这个PID也被改变了。
所以说每一个程序结束以后在重新启动,PID就会变化了,所以他是系统当中动态运行的相关信息。
我们可以进入到它里面就可以看到这个东西,这就足以证明这个就是我们的这个可执行程序的进程
对于这个cwd,就是我们当前进程的工作目录(current work dir)
对于这个cwd,这个是很有用的
比如说当我们使用touch命令创建一个test.c文件的时候,它是如何找到当前的目录呢?
就是进程启动的时候已经记录了当前的目录。所以才找到的
就好比我们之前的文件操作的时候,为什么文件可以创建在当前目录呢?
就是因为进程有这个cwd,记录了当前工作目录,所以可以直接拼接上去。这就是当前路径
7.kill进程
我们先写出如下代码
如下所示,当我们运行了这个代码以后,它就会有它对应的进程
并且其中我们发现最下面的这个proc其实是因为grep这条指令也要带上proc,所以也把他过滤出来了,可见指令在运行时也是要有进程的
那么我们也可以看到这个PID,有了这个PID有什么用呢?在前面我们用它可以找到一个对应的文件。那么还有什么作用呢?其实我们还可以直接杀掉这个进程
kill -9 13388 //13388是当前程序对应的PID
我们可以看到,进程确实被杀死了
8.进程的PID
我们知道对于我们的操作系统,它的内部一定是这样的
内部有task_struct结构体,然后指向对应的代码块和数据。而pid一定是在它的内部存储着的
而我们前面的ps命令其实就是在遍历这个链表。从而获取每个值的
如果我们想要获得某个进程的pid,由于我们无法直接拿去,所以我们只能通过系统调用来获取
getpid()
如下是man手册中的getpid
我们可以知道,它的作用是返回调用这个函数的进程的PID
然后我们可以去应用一下:
我们可以用这段指令来进行检测进程
while :; do ps ajx | head -1 ; ps ajx | grep Proc | grep -v grep;echo "------------------------------------------------"; sleep 1; done
它一开始的效果是这样的
如下所示,我们就可以检测到进程了
我们也可以看到这个进程的PID,并且如果我们在中间终止掉了这个进程,然后再重启的时候,它的PID已经被改变了
相应的,除了PID以外,还有一个PPID,它的意思是父进程
同样的,我们可以去获取他的进程
如下是运行结果
如果我们将这个进程杀掉,然后,我们继续重启这个进程
我们会发现,父进程没有变化,我们自己的进程的ID变化了
那么我们可能会好奇这个13200里面到底是什么呢?
我们可以看到有很多的13200,但是我们先只关心一下PID为13200的这个
我们可以看到它后面跟的是bash,也就是命令行解释器
所以说,运行一个进程时,命令行解释器会把我们这条命令的进程变成bash的子进程,由子进程实行对应的命令,这样的话,当子进程执行命令的时候,一旦这个进程出问题了,就不会影响bash进程
但是如果我们直接将我们的这个Xshell断开链接了,然后重启,我们就会发现这个PPID也变了
但是如果我们只是普通的杀掉这个进程,然后再运行的时候,父进程不改变
然后我们可以像前面一样,调出这个父进程所对应的文件
我们可以看到,以前的13200也就不见了
所以说,我们每次重新登录的时候,系统会为我们重新创建一个bash进程
我们在命令行中输入的所有进程都是bash的子进程
bash只负责命令行的解释,具体执行出问题的时候,只会影响它的子进程
这就是为什么当一个子进程中断的时候,父进程不变的原因
9.fork
我们前面一直在说进程,那么如果我们想创建一个进程,该如何做呢?
一个方法就是我们前面使用的直接./Proc
,这样的话就是操作系统为我们创建进程了
如果我们想要自己创建一个进程,那么我们可以用fork函数
我们先用起来这个函数
当我们用如下代码,不包括fork的时候
毫无疑问,打印两行
当我们将这个fork加上以后
我们会发现打印出来了两次after
为什么呢?这个其实就是因为,fork之前只有一个执行流,forK之后就有两个执行流了。
不过我们还是先看一下fork函数
这是它的返回值,意思是如果成功,那么返回一个子进程的PID给父进程,返回一个0给子进程。如果失败,返回-1给父进程,不创建子进程,并适当地设置errno。
我们发现它好像有两个返回值?
我们先看一下这个代码
如下是我们的运行结果
我们发现这个运行结果很奇怪。出现了很诡异的现象。
这其实就是fork的作用,有了它就有两个进程了
我们可以关注一下一组pid和ppid的值
我们会发现,这两个刚好就是一组父子关系。
因为父进程的pid等于子进程的ppid
而前面这个29958是bash命令行的进程,从下图可以看到
我们也可以在前面的这个图看到,这个父进程其实就是我们原来的进程,而子进程就是这个父进程所创建的一个新的分支
所以这样的话,我们就也可以创建一个进程了
所以
./xxxx------------指令层面创建进程
fork()-------------代码层面创建进程
10.对fork的深刻理解
fork的英文意思是叉的意思,也就是代码一分为二
如下图所示,当我们使用fork以后,代码就变成了两个执行流,父进程的执行流执行id值大于0的部分,子进程的执行流执行等于0的部分
在这里我们就会有如下几个疑问
- 为什么fork要给子进程返回0,给父进程返回子进程的pid
- 一个函数是如何做到返回两次的?如何理解
- 一个变量怎么会有不同的内容?如何理解
- fork函数究竟在干什么?干了什么?
- 为什么fork要给子进程返回0,给父进程返回子进程的pid?
返回不同的返回值,是为了区分让不同的执行流,执行不同的代码块
一般而言,fork之后的代码父子共享
以上回答了为什么要不同的值,下面回答为什么父进程要返回子进程的pid
这是因为,一个父亲可以有多个孩子,而一个孩子只有一个父亲
所以未来父亲需要对孩子做出区分,所以返回一个pid就可以区分各个子进程
而对于一个孩子,由于父亲是唯一的,所以返回0即可
所以必须返回子进程的pid,用来标定子进程的唯一性
子进程只需要调用getppid就能直接获取父进程的pid,所以它直接返回0标记成功即可
- fork函数究竟在干什么?干了什么?(上)
我们知道
进程 = 内核数据结构(PCB) + 代码和数据
如下是我们fork之前的样子,只有当前的一个进程,CPU去调用这个进程
而创建子进程其实就是:系统里面多了一个进程
即如下图所示,首先,会先照着原来的task_struct创建一个新的task_struct。不过这个新的tash_struct会将它的pid修改为一个新的pid,将它的ppid改为前面的pid
现在子进程有了自己的内核数据结构,也就是PCB,那么它还没有它自己的代码和数据,它应该访问什么呢?
而父进程是可以访问自己的代码和数据的。子进程没有,所以只能去访问父进程的代码
所以说,他们的代码是共享的
所以CPU在执行的时候,如果是父进程,跑的是它的代码,如果是子进程,跑的也依旧是这个代码
即fork之后,父子进程代码共享,因为代码是不可以被修改的
那么既然代码是共享的,那么之前的数据呢?我们先不考虑这个问题
我们先来看一下这个,既然代码是共享的,那么我们为什么要创建进程呢?
这是为了让父和子执行不同的事情,所以需要想办法让父和子执行不同的代码块,所以我们就需要让fork具有不同的返回值
- 一个函数是如何做到返回两次的?如何理解?
在上一个问题中,我们讨论到了代码块一定是共享的,为了让其可以执行不同的代码块,就需要具有不同的返回值。
那么这个是如何做到的呢?
我们要注意,fork本身也是一个函数,即它也有自己实现的代码
当一个函数已经return的时候,它的核心工作已经做完了。这就意味着。在返回之前已经创建好了子进程了。
而在return之前函数体内部一定会发生下面这些事情
- 创建子进程PCB
- 填充PCB对应的内容
- 让子进程和父进程指向同样的代码
- 父子进程都是有独立的task_struct,可以被CPU同时调用运行了
- …
而创建好子进程后,由于父子进程代码共享,而return也是代码,所以return也是会被共享的
所以父进程调度执行的时候,返回一次,子进程调度执行的时候,也返回一次
所以这个fork函数最终返回了两次
- fork函数究竟在干什么?干了什么?(续)
前面说过,子进程创建好PCB以后,会共享同一块代码,那么数据呢?
首先在任何平台,进程在运行的时候,是具有独立性的,也就说当我们的qq崩了以后,并不影响其他软件
所以,父子进程既然是两个进程,那么他们就不能同时访问同样的空间。
因为数据可能会被修改,所以不能让父进程和子进程共享同一份数据。
而代码是不可被修改的,所以可以共享同一份代码,因为并不影响独立性。
所以说,子进程要将这份数据单独拷贝一份
可是有可能父进程创建出来的变量,子进程并不会去使用。这样使得子进程的利用率很低
所以操作系统变决定不这样直接去拷贝,而是一开始先共享代码和数据,但是当子进程要对某个数据进行修改的时候,采取拷贝一份这个这个数据。
所以只有当子进程要尝试修改某个数据的时候,才会去拷贝一份。这也叫做数据层面的写时拷贝
- 一个变量怎么会有不同的内容?
当我们理解了fork函数究竟在做了什么,我们也就随之理解了一个变量怎么会有不同的内容
return就是写入的过程,id就是父进程的数据。
所以对于子进程的id就是就会专门创建一个空间用来存储,要发生写时拷贝
所以最终返回值是不同的
所以说,我们在看我们之前的代码,我们也就能理解这些现象了
- 当父子进程被创建好,fork以后,谁先运行呢?
其实谁先运行是不确定的,是由调度器来决定的
也就是说,现在有很多个进程,CPU应该挑选哪一个进程来运行呢?这个是由调度器来决定的
所谓调度器,就是一套执行数据结构查找的相关算法,它会在这些进程中找到一个合适的进程放到CPU中。
像我们现在的计算机,调度器要尽可能保证公平,时间都比较均衡。
11.bash用fork创建子进程
我们还是看下面的运行结果,代码同上
我们现在已经差不多了解了这些PID和PPID这些信息了。
那么对于原来的那一个进程而言,它也是一个子进程为11129,它的父进程8464
而这个8464正好就是bash命令行解释器的进程。我们也知道这是为了保证该进程不会影响其他进程的
那么既然这里是通过创建子进程的方式完成的,那么bash如何创建子进程呢?
其实就是利用fork,让这个fork出来的子进程去执行解释新的命令。而它自己就继续接收用户的输入。