🪐🪐🪐欢迎来到程序员餐厅💫💫💫
主厨的主页:Chef‘s blog
所属专栏:青果大战linux
每日小感慨:
最近很羡慕学校小登,感觉他们还刚上大学,有无穷的潜力与可能性,而我已经行将就木了
在学这节课前请先观看上节课冯诺依曼体系结构 & OS的概念
进程的概念
或许有人在教材上发现了这样的定义:
一段正在运行的程序就是进程。
我承认他的话并没有错,但这只是进程的直观表现,想真正了解它我们得进入linux更深层次去看。
我们知道上节课学到OS是一款负责软硬件资源管理的软件,程序显然属于软件,那OS就要对其进行管理,我们上节课又学到管理的本质不是管理你本身(这个看的见摸得着的存在),而是对你的数据进行管理分析,然后做出决策。所以OS管理程序是必须要获取程序的数据(创建时间、创建路径,创建人等等)。在获取硬件信息时直接加载他的驱动就好了,可是驱动本身就是程序,所以在获取程序数据时,OS最后一定是从要和程序进行了交互,但上节课的冯诺依曼体系结构告诉我们,OS与外设上(即硬盘)的程序(即代码)打交道是要先把程序加载到内存才可以的。
与此同时,OS要去管理的程序显然不止一份,要管理如此多的程序要怎么办呢?
具体过程就是先用结构体存储数据以便描述程序,接着依靠数据结构把多个结构体组织起来,完成管理操作。
我们称这个结构体为PCB结构体,在linux中PCB的名称是task_truct。这个结构体放了很多东西,
我们进一步思考OS的管理操作,
例如cpu的使用,cpu只有一个,但程序有很多,这就像你去医院看病要排队一样,程序想去cpu上执行自己也要排队,但是我们并不需要程序本身去排队(因为它里面没有指针无法有效管理),只需要让他对应的PCB去排队就好了,把他们对应的next指针连接起来,就得到了一份链表,然后依次向后遍历,到那个节点就让他去cpu运行一会。
比如等待队列也是一个链表,他把要等待同一个设备(如键盘输出)的进程按照一定的顺序以链表指针的方式链接(在task_struck里加一个next指针即可),然后依次向后遍历,到那个节点就让他去接受键盘输出。注意我们比不需要新建一份PCB,只需要在PCB中放置对应链表就可以
即产生新的数据结构只需要在PCB加指针,而不需要重新拷贝一份PCB
如此,OS先是通过task_struct描述出了所有程序,接着又根据具体需求加入不同指针来对他们进行管理,也就符合了“先描述,再组织”的核心。
这时我们就知道了,在数据层次上,
进程=task_struct(内核数据结构)+自身代码和数据
进程查看
ps指令
我们自己写一段程序让他运行试试。
我们写了一个死循环,接着每隔一秒输出一个“666”。
运行起来后,结果符合逾期
接着我们用ps -ajx可以查看进程的各种信息(等同于windows系统下的任务管理器),由于杂乱信息太多于是我们grep一下
显然上面的就是我们要找的myproc3进程,而grep要查找myproc3,所以自己也带有myproc3,于是也被选中了,放在下面。
第二列的数字是每个进程的PID,进程的唯一识别符。
我们可以通过getpid的方式获取进程的PID,getpid是一个库函数,包含在<unistd.h>,他的返回值PID的类型是pid_t,本质是还是int,包含在<sys/types.h>
代码如下
#include<iostream>
#include <sys/types.h>
#include <unistd.h>
using namespace std;
int main()
{
pid_t id = getpid();
while(1)
{
sleep(1);
cout<<"pid ="<<id<<endl;
}
}
我们通过ps查看该程序的PID,发现确实是7038
-
PPID指的是父进程,也就是创建该进程的进程
-
PID是该进程的唯一标识符
-
COMMAND是运行的可执行文件的名字
接下来我们可以通过getppid进行查找父进程的PPID
#include<iostream>
#include <sys/types.h>
#include <unistd.h>
using namespace std;
int main()
{
pid_t id1 = getpid();
pid_t id2 = getppid();
while(1)
{
sleep(1);
cout<<"pid ="<<id1<<endl;
cout<<"ppid ="<<id2<<endl;
}
}
通过ps查找2698这个PID,我们发先它对应的可执行程序叫做bash
这里先提出一个观点:
命令行调用的一切进程,都是bash的子进程
为什么?之后会讲
proc目录
我们在根目录下可以找到proc这个文件夹
接着我们打开根目录下的proc文件,我们也确实找到了2698和7650这个PID
接着我们打开7650,而这文件里装的就是该进程的各种信息
我们选个重点的讲一下
-
cwd:表示当前进程的工作目录。它指向进程的当前目录,影响文件操作的相对路径。
-
exe:表示进程执行的可执行文件的路径。
在了解这个后我们在看下面的代码
我们直到fopen的第一个参数可以是绝对路径,也可以是相对路径,绝对路径很好理解,可是相对路径呢?程序执行的时候怎么找到这个相对路径呢?就是依靠这个进程参数中的cwd
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
int main(){
pid_t p=getpid();
FILE *f=fopen("test.txt","w");
if(f==NULL)
printf("failure\n");
while(1)
{ printf("pid:%d\n",p);
sleep(1);
}
return 0;
}
根据上图发现这个文件也确实是在该进程被启动的目录下所创建的
然后我们在kill -9 PID,直接删了该进程,再去查找这个文件夹就发现该文件不存在了
说到这里大家就懂了,当程序(可执行文件)被启动时,OS会在proc下给他创建一个文件,里面存储他的种种信息,而当该文件结束时,这个文件会被删除。事实上,如果你在启动程序后直接关闭Xshell,当你在打开时会发现该程序自动结束了,而他在proc中的文件也被删除了。这其实说明了该文件掉电就丢失,即:proc文件是放在内存中的!
为什么不方到硬盘呢?因为放硬盘的话
-
加入程序意外停止(比如拔电源),那这份文件就会一直保存在硬盘中,可是下次再启动时会建立新的文件,所以他已经毫无用处了,那我们的磁盘空间就被浪费了。
-
假如ps这些获取进程数据的指令启动了,那cpu就需要从磁盘取数据给他们用,而由于冯诺依曼结构,cpu又要先把数据加载到内存,那我们不如直接把该文件放到内存,省去了从硬盘到内存的时间消耗,至于空间,反正内粗现在都是8G,16G,正常状况下还是完全够的。
于是我们得出结论,在创建一个进程时,OS会把他的信息加载到内存文件中
小结,在启动程序时,OS先创建的ask_struct,然后去访问task_struct得到对应数据,填充给/proc创建对应的文件夹,当使用ps这种查找进程信息的指令,则会到对应的文件目录下进行查找。当程序关闭时,该文件会被删除。
fork函数
fork函数可以在进程内部创建一个子进程,他被包含在头文件<unistd.h>中,直接调用即可。
可是,fork的返回值很奇怪,他是这么介绍的
- 如果创建失败,就给父进程返回-1(这没问题)
- 如果创建成功,就给子进程返回0,给父进程返回子进程的PID
what?请注意,我们用的fork是一个函数,而现在他说“如果成功,会有两个返回值”
这怎么可能,匪夷所思。于是我们来测试一下
#include<iostream>
#include<unistd.h>
#include<sys/types.h>
using namespace std;
int main(){
pid_t p=fork();
if(p>0)
{ pid_t p1=getpid();
pid_t p2=getppid();
cout<<"我是父进程:我的pid是"<<p1<<"我的父进程是"<<p2<<endl;
}
else if(p==0)
{ pid_t p1=getpid();
pid_t p2=getppid();
cout<<"我是子进程我的pid是"<<p1<<"我的父进程是"<<p2<<endl;
}
else
cout<<"创建失败"<<endl;
}
可以看到我们确实创建了两个进程,根据PID和PPID也确实是一父一子
但是,if语句和else if都跑了起来。
我们可以开始思考了,fork函数可以创建一个进程,我们现在知道了进程就是task_struct+代码和数据,taskstruct暂且不管,请问子进程的代码和数据怎么来呢,显然只能是来自一份父进程的代码和数据,我们有知道代码加载到内存后无法修改,所以父子进程可以都使用父进程的代码,所以只要让子进程的taskstruct里的指向代码的指针,指向父进程的代码即可 。可是数据不行,在代码运行时数据会不断变化,如果父子进程公用一份数据,那相互直接就会影响,于是子进程需要把父进程的数据拷贝一份,至于怎么拷贝,拷贝到哪里,我们之后再说。
于是我们就知道,fork函数会把他后面的代码和数据当作子进程的代码和数据。
进一步思考,虽然我们还不知道fork怎么创建的子进程,但是我们可以粗略的将其分为两部分
第一:先创建子进程,第二:返回一个值。
显然返回值这条return语句是整个函数最后一条语句,此时子进程已经建立好了,所以return语句也是父子共享代码的一部分,而这里的返回值也是一份数据,是数据那子进程就要拷贝一份,所以fork有两个返回值的本质就是向原来的数据和这个拷贝的数据分别进行写入!于是乎,我们就理解了fork的双返回值实现原理。
为什么返回值一个是0,一个是子进程的PID呢
以为对于子进程来说,他想找到父进程直接getppid就可以,但是父进程想找到子进程无法直接调用函数找到,根本原因是一个儿子只有一个父亲,但一个父亲可以有多个儿子,所以为了让父进程知道自己创建的子进程是谁,就需要用返回值的形式告诉他。
总结
-
进程就是task_struct(内核数据结构)+代码和数据
-
ps -ajx可以查看进程信息
-
进程的信息都放在/proc这个文件夹里,这个文件夹用的是内存空间,进程退出,其信息会被删除
-
fork函数可以创建子进程,和父进程共用代码,拷贝一份父进程的数据
-
我们可以通过fork返回值分别控制父子进程,