【Linux】进程学习① (进程的PCB(task_struct)&&进程的标识符详解&&进程的创建fork函数)
时间:2024-04-13 12:55:24 来源:网络cs 作者:康由 栏目:电商平台 阅读:
目录
编辑
1.进程的概念
1.1进程的描述与组织:进程的PCB
进程:进程是 内核pcb对象+可执行程序/内核数据结构+可执行程序=进程
1.3 task_struct
2.PCB内部属性
3 查看进程
4.获取进程标识符:getpid函数(4-6主要围绕进程的标识符展开)
5.父进程标识符获取getppid函数
另外一种查看进程方式:
cwd
6.创建进程fork
6.1 fork函数的返回值
6.2 父进程的代码和数据
6.3 关于fork函数的返回值:
6.3.1 如果创建进程成功:返回子进程pid给父进程,给子进程返回0,创建失败返回-1. 这是为什么?
6.3.2 函数有两个返回值,至少应该返回两次,所以fork函数为什么能够返回两次呢?
6.3.3 写实拷贝(对父子进程数据的解释)
6.4 一次创建多个进程:
7.结语
1.进程的概念
根据冯诺依曼计算机结构,首先什么叫做程序?
程序本质就是二进制文件,C语言经过编译器编译生成可执行程序后,在磁盘放着。磁盘本质上是外设(输入输出设备),双击运行,首先要将程序加载到内存中,再交给cpu运行(才得以被cpu读写,解释)
其次,什么叫进程?
教材观点:把程序加载到内存叫进程或者正在运行的程序。
我们要理解好什么叫做进程。理解计算机各种工作的第一性原理:先描述再组织。
切入点:在操作系统或者计算机体系中,可以同时运行多个程序
任务栏空白处单击右键打开任务管理器可以查看
这是一个事实:我们可以同时启动多个程序
1.1进程的描述与组织:进程的PCB
每一个程序背后都有一个exe文件, 那么启动多个程序也就意味着将多个.exe文件加载到了内存中,那么操作系统要不要管理多个加载到内存中的多个程序呢?答案是要的,那么操作系统如何管理这些加载到内存中的的程序呢?操作系统肯定是先处于内存中,因为开机就启动了操作系统这个软件,就已经加载到内存中了
下一步:操作系统如何管理:
先描述,再组织
先描述一下这个加载到内存中这个程序的属性可以使用结构体,l因为inux操作系统是用c语言写的
struct xxx
{
状态;//活着还是死亡的
优先级;//cpu只有一个,决定执行先后
内存指针字段;//要运行这个应用程序,这个应用程序的代码在什么地方,去哪里寻找这个程序的代码和是数据
标识符;//每一个程序的标识标号
::
::
::
要包含进程几乎所有的属性字段
struct xxx * next;
|}
那么当每一个可执行程序也可以叫做进程被加载到内存中时,只有可执行程序的代码和数据,操作系统并不认识加载到内存中的每一个程序,就好比我们刚入学,院长根本不认识我们,那么学校就说,为了下次我知道你们是谁,也为了让班主任、学院好管理认识你们,我给你们每个人一个学生卡,上面就有学生的名字、班级、学号、等等信息。那么操作系统将可执行加载到内存的同时,为了更好管理,操作系统就会给每一个可执行程序、进程创建对应的描述该进程的属性的结构体变量或者对象,把属性补充好,所以每一个进程都有一个对应的结构体变量。这个结构体变量中保存的是每一个进程的属性,在官方的描述中我们称这个结构体变量为PCB(process ctrl block),中文名称为:进程控制块。于此同时,每一个pcb内部还包含指向下一个pcb的指针,pcb创建的时候也是要占据空间的,所以当程序加载到内存中,占据的空间比程序的大小要大,多出来的空间就是我们的进程控制块。
从此,操作系统通过pcb可以访问各个进程,然后进每个进程的pcb组织起来:
对进程的管理就变成了对pcb链表的增删查改。如果此时新增加一个程序,对新程序创建pcb结构体,然后填充属性,分配编号,连入pcb链表。,输出释放进程,将代码删除将pcb描述删除,进程就删除了。
所以
进程:进程是 内核pcb对象+可执行程序/内核数据结构+可执行程序=进程
我们的pcb链表可以理解为数据结构链表,只不过我们当时使用一个data来代表每个节点的数据,这里的单个pcb就可以理解为单个的节点,每个节点的数据域有百多个属性这样。那么我们的数据结构还有队列这样的数据结构,那么我们的cpu是如何运行进程的呢,将进程pcb入队列,所以这就是让进程运行的时候排队,实质上是让进程的pcb进行排队,进程可以动态被调度的实质也就是将进程的pcb放入运行队列里面,等待cpu调度。
结论:所有对进程的控制和操作,都只和进程的pcb有关,和进程的可执行程序没有关系。如果愿意,也可以把pcb这个(广义可以理解为一个节点)放入都任何数据结构中。放到不同的数据结构就相当于放到不同的容器之中,就可以进行各种各样的组织管理模式。各种增删查改,旋转等等,都是对节点的管理,节点里面包含的就是数据,所以数据结构所有的动作模拟的都是对进程的管理工作动作。节点表示是人就是对人数据进行管理,节点表示进程,表示驱动,表示硬件就是对进程、驱动、硬件进行管理。
PCB是操作系统学科的叫法,上述结论对于任何操作系统都是一样的适用,不过PCB只是宏观学科的称谓,到具体的操作系统中,叫法可能不一样。
1.3 task_struct
在linux中,linux是一款具体的操作系统,将这个操作系统的PCB成为:task_struct
strruct就告诉我们这是一个结构体,task有时也将进程描述为任务。
里面包含了经常的所有属性,可以在linux源代码中进行查看
2.PCB内部属性
task_struct 内容属性分类(粗粒度了解一下)
标示符: 描述本进程的唯一标示符,用来区别其他进程。状态: 任务状态,退出代码,退出信号等。优先级: 相对于其他进程的优先级。现实生活中,很多问题都与优先级有关,我们到银行取钱要排队,排队在前优先级高,但是优先级诞生的原因就是资源过少,窗口少,办理人多,我们电脑多数只有一个cpu,这就注定了进程要排队执行。程序计数器: 程序中即将被执行的下一条指令的地址。我们写下代码让编译器执行,我们的编译器怎么知道当前执行到哪一行了,下一行要运行到哪里呢?在cpu计算机内部存在一个寄存器,叫做pc指针或者eip寄存器,也叫作指令寄存器,也叫作程序计数器。pc指针指向哪一个程序的代码就表示那个程序被调度运行。
内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针pcb如何找到进程,如何找到进程对应的代码和数据,那么能帮助pcb找得到程序的代码和数据的部分就叫做内存指针。
上下文数据: 进程执行时处理器的寄存器中的数据I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。进程拥有那些设备信息,比如printf要打印,屏幕使用权申请先给了这个函数,使用完再还给操作系统记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。其他信息
3 查看进程
ps axj命令
pid --process id 进程的唯一标识符
command 代表启动时对应的可执行程序
写一段代码,运行变成进程
查看:
ps axj | head -1 && ps axj | grep myprocess
grep 是负责过滤我们的myrocess这个关键字,当他要过滤时,关键字就会包含这个关键字,自己也会变成进程,又包含这个关键字,就会被保留。
这也说明每个独立的指令都是一个进程。
不想要grep -v反向匹配:
ps axj | head -1 && ps axj | grep myprocess | grep -v grep
如果是这样简单的程序:
瞬间启动, 变成进程,操作系统立马将代码加载到进程,创建pcb,被cpu调度,不过这个进程执行太快了。所以计算机是很快的。
看到进程退出:
每隔一秒检测进程
while :; do ps axj | head -1 && ps axj | grep myprocess | grep -v grep; sleep 1; done
现在没有运行程序,没得对应进程,我们运行起来
然后我们结束程序
这就是一个进程从无到有再到无,进程是有生命的。
正确的叫法:将程序运行起来变成进程。
进程还有很多属性
4.获取进程标识符:getpid函数(4-6主要围绕进程的标识符展开)
PCB属性数据属于内核还是操作系统?
PCB整个结构体变量和内部数据虽然是进程属性,但是是在操作系统内部维护,属于内核数据结构,属于操作系统内部结构。所以一个进程所有数据是在操作系统内部维护的。
那么如果我们想要获取我们进程的标识符,也就是获取进程的Pid,就是用户想要访问内核数据,按照上节内容讲的,那么我们必然要调用系统调用才可以做到。
这个系统调用接口为函数:getpid,默认认手册二号手册
默认认手册二号手册
函数功能由系统内核提供
头文件:《sys/types.h》
函数功能;获得一个进程的标识符信息
参数为空
返回值类型:pid_t,这是系统自己定义的一个类型,相当于C语言中的整数类型。
誰调用就获得誰的pid
开始mian函数的时候,已经是cpu在执行指令了。
5.父进程标识符获取getppid函数
在linux中,一般普通进程都有它的父进程
当前进程是被父进程创建出来的。
ppid -- 就是父进程的标识符
任何人都只有一个亲生父亲,但是一个父亲有多个孩子
getppid 获得父进程的标识符
但是我们发现每次启动,进程的pid都会发生变化,但是父进程的pid不变:
每次启动进程都是被看做一个新的进程来看待。 父进程没变说明我们的
进程每一次调度都是由一个父进程来调度的,我们通过这个父进程标识符好来看一下这个父进程是誰:
是我们的bash ,所以在命令行中启动的所有的程序转换为进程都是bash的子进程,bash是命令行解释器。
另外一种查看进程方式:
linux把进程相关的内存级数据,以文件系统的形式显示在文件目录中
在根目录proc路径下:
我们可以像查文件一样查进程,而且这个文件是实时的。
当我们关掉进程绝没有这个文件了
所以linux会将一个进程的进程信息以 进程pid命名的目录中:
我们查看一个进程的信息:
如果在运行的时候我们将可执行程序删除掉:
cwd
这一行闪动爆红
进程还在跑,原因:
进程对应的可执行程序就是我们删除的那个,我们运行程序的时候,将程序拷贝到内存中,所以不会影响本次进程,但是重启就,没有办法了。程序运行时要记得自己对应的可执行程序是谁。
cwd 是可执行程序所在的路径
create work direct 当前工作目录,
6.创建进程fork
我们平时创建一个进程的时候,只是运行,操作系统会为可执行程序创造一个进程,我们是手动启动一个进程的,那么我们可不可以使用函数创建一个进程呢,是可以的
fork函数创建子进程。
①系统调用
② 作用:创建一个子进程
③参数为空
④头文件《unistd.h》
⑤返回值:pid_t
编写代码如下:
after打印两遍,说明fork之后是有两个执行分支。都执行了printf
根据上图pid的关系就可以知道fork之后一个是父进程一个是子进程,而且fork之后代码共享。
怎么知道那个是父进程,那个是子进程呢?
6.1 fork函数的返回值
如果创建进程成功:返回子进程pid给父进程,给子进程返回0,创建失败返回-1.
访问同一个变量id,怎么会等于两个值, 先简单理解:如果是父进程来读这个id变量,如果创建成功父进程读到的就是子进程的pidd,如果是子进程来读,如果是创建成功的话,读到的就是0.
那么我们创建进程的目的是要么这件事父进程不做交给子进程去做,或者父子进程做不一样的事。所以才要创建进程,一般都是父子做不同的事。
所以我们fork级别创建子进程的时候,一般我们会这样写:
我们具体实现,可曾见过两个死循环一直跑
ps指令可以查到两个进程。
每一个进程 = 内核数据结构+可执行程序
6.2 父进程的代码和数据
创建父进程的时候会同时建立父进程的task_struct 来维护父进程的代码和数据,实际上子进程的创建是操作系统创建的,父进程执行代码创建一个进程,创建一个进程的时候,系统中就会多一个进程,同时操作系统层面要为子进程创建独立对应的task_struct,每一个进程创建的时候,也就是说父进程或者历史上的其他进程,除了操作系统创建的task_struct以外还天生的有自己的代码和数据,但是我们由父进程创建的子进程,有自己的task_struct,却没有自己的代码和数据,所以,子进程某认就会指向父进程的代码和数据。儿子说:你给我生出来了,又不给我买房买车,我只能坑老,挤在一起。父进程不愿意,就将自己的代码划分,自己和子进程一个执行一块。
fork之后代码共享,可以使用if else 来进行分流让父子进程执行不同的代码。这是代码层面,数据层面稍后。
cpu在调度的时候,不会管父子进程,只会正常调度,所以父子都会调度,所以两个while死循环才会都执行。同时,操作系统在为子进程创建task_struct的时候,父进程也会将自己的task_struct中的属性大部分的拷贝给子进程。所以两者才能看到相同的代码和数据。所以子进程会继承父进程的大部分属性。这种继承不是百分之百,至少pib ppid 等不同。
6.3 关于fork函数的返回值:
6.3.1 如果创建进程成功:返回子进程pid给父进程,给子进程返回0,创建失败返回-1. 这是为什么?
从结果来看确实两个if都执行了,说明我们的id确实满足两个条件,可是是一个变量 ,应该变量怎么等于0又大于0,首先系统调用是操作系统单独设计的。在现实生活中,每一个人只有一个父亲,但是一个父亲可以有多个儿子。我们的进程也是这样的,如果我们使用子进程去找父进程是比较好找的,因为父进程具有唯一性的,父进程找子进程就必须使用标识符(家里几个兄弟姐妹,叫一声爸爸,就可以找到,但是爸爸找儿子就不会叫称谓而是叫名字)所以,我们会给父进程返回子进程pid,方便我们的父进程对子进程进行唯一标识,后续便于管理,那我们的子进程不需要父进程的pid,关心自己创建成功没有。子进程很容易得到父进程的pid,父进程唯一。
6.3.2 函数有两个返回值,至少应该返回两次,所以fork函数为什么能够返回两次呢?
fork 刀叉,分支的意思,,fork只有一个进程在运行,所以只能是父进程调用fork。那么我们思考。如果一个函数,已经运行到了最后开始执行return 的时候,一般这个函数的核心逻辑是已经做完了(不考虑递归),fork也是函数,只不过是系统调用的接口,fork会以调用进程为模版创建子进程,然后把对应当前进程的pcb放入到运行队列里面,所以子进程也就开始运行了,那么当fork运行到return 使,是不是已经将上述工作已经做完了呢?答案是肯定的,已经做完了,而且父子进程都已经可以被调度了,return 作为一个语句,所以在执行return 语句前已经有了父进程,子进程,所以机会分别执行一次,那么return 就会执行两次。但是fork返回时是用一个变量接收的,这个变量怎么会等于0也大于0呢,(关乎进程地址空间),先简单了解:我们将电脑上所有程序打开,就有很多进程启动运行了,一个程序崩溃,并不会影响其他进程,那么当我们关闭掉父进程或者子进程中的一个会不会影响我们的另外一个进程呢?
向指定进程发信号杀掉进程:
kill -9 pid
杀掉父进程过后,只有子进程在运行。 两个不影响
结论:任意进程之间是具有独立性的,互相不能影响。(bash干掉一般也不影响已经运行起来的进程)父子进程一个退出,即便代码共享也不能互相影响,一个挂了,代码也不能回收,等两个都结束再回收,但是当两个进程对数据进行改变的时候,可能会互相影响。
6.3.3 写实拷贝(对父子进程数据的解释)
os在设计进程的时候就必须保证进程得有各自的独立性,当父子任何一方要对数据进行更改,操作系统会介入进来,操作系统会拷贝对应的变量到子进程中,单独修改,父进程也是如此,我们将这个过程称为:写实拷贝,写实拷贝发生后也就意味着父子进程使用不同的空间。
所以id有两个值就是系统帮我们做的,可以用一个变量名表示不同的内存linux是可以做到的。
返回的本质也是写入数据,虽然是同一个变量,但是发生写实拷贝,虽然是一个名字,但是是对应的父子进程各自使用的不同的空间,也就是不同的值。我们从编译的角度理解,当我们写C语言代码的时候,将值放入变量,但是变量名不过是我们给存储值的空间取的代号,编译器编译的时候,变量名也就不在了,会转化成地址,或者某种寻址方式,所以我们代码层面看到的是id,编译器看到的是不同的两个地址。
6.4 一次创建多个进程:
代码如下:
主进程会一直创建子进程,子进程慢慢倒数知道进程结束。
7.结语
今天的主要内容是进程、进程的pcB,以及linux下进程pcb-task_struct中众多属性的第一个进程的标识符。扩展了很多内容,后续更新进程的状态,僵尸进程、孤儿进程的讲解,欢迎大家关注。创作不易,如果大家觉得有所收获,欢迎关注,一起交流互进。
本文链接:https://www.kjpai.cn/news/2024-04-13/158047.html,文章来源:网络cs,作者:康由,版权归作者所有,如需转载请注明来源和作者,否则将追究法律责任!
上一篇:一文让非技术宅读懂为什么AI更“喜欢”GPU而不是CPU?
下一篇:返回列表