【进程篇】理解进程
进程
基本概念与基本操作
什么是进程?通俗点来理解:
我们的可执行程序在磁盘里,二进制文件,加载到内存里是代码和数据,这就是进程吗?
但磁盘上可能有成百上千的可执行程序,其中可能有50个100个全都加载到内存了,所以可能在同一时刻,系统内部同时被加载了非常多的可执行程序吗?答案是一定。
这是都应用程序,都被加载到内存中了。(冯诺依曼规定)
然而在最开始就有一款软件已经被加载到内存了:操作系统
我们在开机等待时,操作系统在启动。
这些被加载进来的这么多程序在内存的什么位置加载?这些代码和数据是否已被CPU执行完了呢?是否要释放空间、扩容空间等?调度?操作系统不知道这些代码和数据属于哪个可执行程序。操作系统必然要对多个加载到内存中的程序进行管理。
怎么管理呢?先描述再组织!
操作系统要给每个加载进来的代码和数据构建一个struct维护起来。
这些结点能够指向自己对应的代码和数据,结点里包含所有的属性,同时结点还能指向下个结点。
我们将这个程序列表叫做进程列表。
所以并不是把我们的可执行程序的代码和数据加载到内存就叫进程了,
这样的一个才叫进程。
进程=内核数据结构对象+自己的代码和数据
操作系统里这个结构体就叫做PCB。所有操作系统下描述进程的都叫PCB。
PCB全称:process control block 也就是进程控制块
具体,在Linux下, 这个结构体名称是task_struct
PCB和tast_struct是抽象和具体的关系,就像shell和bash
进程的所有属性都可以直接或者间接通过task_struct找到
所以刚才说进程=内核数据结构对象+自己的代码和数据,现在又可以说:
进程=PCB(task_struct)+自己的代码和数据
对进程的管理就是对进程链表的增删查改。
一旦一个可执行程序加载到内存里,这个可执行程序自己是最不重要的。最重要的是操作系统要创建对应的PCB来描述它。CPU管理的也是PCB而不是我们自己的可执行程序。
为什么要有PCB?因为要描述,先描述再组织来管理进程。
task_struct里有什么?
在Linux的源代码中可以看到task_struct的定义(双链表管理)
可以看到PCB的属性规模非常大
现在我们还是用实操来更好地理解:
我们用sleep,头文件是<unistd.h>
结论:我们历史上执行的所有的命令、工具、自己的程序,运行起来全部都是进程!
getpid
那么怎么看这个进程相关的属性值呢?
人生的第一个系统调用:getpid
系统里如果一个进程自己启动了,要获得标识符,就用getpid。谁调用的这个函数,就获得谁的进程id
从这个头文件也能看出是与系统有关的。
为什么说这个getpid是个系统调用?查看的是2号手册
man man
可以看到:
3号手册是库调用,2号是系统调用。
pid在哪里?在task_struct这个结构体里有个属性叫做pid。
获取pid的本质是让操作系统把当前进程的PCB里的pid拷贝出来,让用户看到自己的id是什么。
有id就能证明是个进程。
pid的p代表进程,id是identity。
我们看到返回值是pid_t,这是个系统提供的数据类型,不是C语言内置类型。其实也就是个整数。
获取成功就是一个大于等于0的值,获取失败就是一个-1
可以看到,确实是一个进程,因为有进程id。
ps
接着,此时我们可以再打开一个Xshell
我们也有指令能让我们去查当前系统里我们所启动的进程有哪些:
ps axj
这样就展现出了当前系统中所有的进程。也可以用top来查进程,退出就是q
但现在如果我们只想看刚才启动起来的进程呢?
ps ajx | grep myprocess
我们看不懂这个,可以将列表头和这个进程一起查看:
也可以将;换成&&
看到这个:
这是什么?
当我们查进程时,这样的grep选项总是会被显示出来,因为整条命令从左向右查的时候,grep也是个命令。grep一旦跑起来自己也是个进程。
我们也可以 查看不包含grep的:
ps ajx | head -1 && ps ajx | grep myprocess | grep -v grep
ctrl+c
ctrl+c 在命令行中如果输入错误了可以这样来终止
它其实是杀掉进程
但,还有其他方法来杀掉进程
同一个程序,每一次启动,pid的值不一样。
kill
kill -9 pid
-9是信号,这里的pid根据情况替换成我们要杀掉的进程的pid
补充两个小知识:
在操作系统中药完成任何任务都是进程。
所以用户是通过进程的方式来访问操作系统的。
如果说用户是老师。操作系统是学生,老师是给学生布置任务的。所以进程也可以叫做任务,所以PCB
在linux具体的类型名是task_struct。其实就是任务。
ls /proc
也就是我们还可以通过文件的方式来查看进程。
操作系统不仅可以把磁盘上的文件像这样用ls查到,还把内存相关数据也以文件方式呈现,让我们能够动态地看到内存相关的数据。
像proc就是一种内存级的文件系统,和磁盘没有关系,所有数据都是内存里的数据。
我们在系统中查到的这些都是数据,无论来源是磁盘还是内存,最终都以文件的形式呈现,让我们动态查看。
这也符合linux下一切皆文件的观点
甚至可以把每个进程转化成若干个文件
根目录下存在一个proc目录,白色字体显示的这些我们不懂但蓝色字体的肯定是目录:
ls /proc -l
但这些目录都是编号,其实就是特定进程的pid。
ls /proc/21150 -dl
我们可以查看pid为21150的进程的目录
这个目录里面就是描述这个进程的很多信息。
总结:
proc目录里记录的是当前系统里所有进程的信息,而当前目录下以数字命名的文件就是特定进程的pid,目录里面包含的是进程在运行时的动态属性,进程一旦退出,该目录会被系统自动移除。
ls /proc/21381
也看不懂
ls /proc/21381 -l
这些众多属性里我们可以先看两个
exe
这个进程对应的可执行文件
进程知道自己是从哪来的,启动哪个指令才有了这个进程
如果我们把这个文件删掉,这个进程还会在跑。
这是因为我们的代码和数据已经从磁盘拷贝到内存了,删除我们删除的是磁盘的。没有影响(后面可能会影响)。
当我们再查一次属性,就开始标红和闪了:
cwd
current work dir
就是当前可执行程序所在的路径
曾经在学文件相关内容时说过
fopen("a/b/c/d.txt","w")
;或者fopen("d.txt","w");
不带路径,就会在当前路径直接以这个文件名新建文件。
为什么不带路径会在当前路径下新建?
是因为进程会记录下自己的当前路径。fopen也是进程下的代码,所以我们只传了文件名而fopen内部会想办法获取当前的工作路径。会将文件名跟到当前路径后面,拼接在一起。所以新建的文件就在当前路径下。
真的是这样吗?
可以看到,就在当前路径下新建了这个文件。
更改一个进程所处的当前路径:
chdir
这样,进程在启动时先把当前路径改了,改完再创建这个文件
再次启动
看到cwd确实被改掉了
再查看当前路径,也不存在刚才的新建文件了:
所以我们的cd命令是怎么做路径切换的?所以bash是不是个进程?shell命令行解释器自己是不是个进程?所以当我们执行cd命令时shell怎么会根据绝对路径、相对路径切换路径呢?底层也就是chdir
回到主线,我们讲的是怎么查进程,一个是ps,一个是proc,还有top也可以。
补充讲了pid,exe,cwd
getppid
那么,我们刚才还看到了getppid,这是什么?
get parent id 获取父进程ID
linux中所有的进程都是被它的父进程创建的,linux系统是一个单亲繁殖的系统,只有父进程没有母进程。
一个父进程可能创建多个子进程。
linux中进程的结构也是多叉树,每个结点都是个进程。
每次重新启动进程pid都变化了,但是父进程id不变。
bash
那么,父进程是谁呢?
我们可以根据pid查:
ps ajx | head -1 && ps ajx | grep 19811 | grep -v grep
我们在这里把原本的grep myprocess改成了grep 19811也就是可执行程序的名字换成pid,来查父进程。
然后会发现我们查到的进程是一个bash ——命令行解释器
1.命令行解释器:本质是一个进程!
2.bash和命令的关系
其实每次登录,操作系统会给登录用户分配一个bash
如果我们
while :; do ps ajx | head -1 && ps pjx | grep 'bash' | grep -v grep ; sleep 1; done
就可以看到,我们现在打开了几个xshell就有几个-bash,关掉一个就少掉一个:
bash是一个进程
那么命令行是什么?
这是bash打出的一个字符串,bash打出来后就在这里等,bash也是C语言写的,相当于一个printf打出字符串再一个scanf在这等待
所以我们的命令都是以字符串交给bash进行分析
其实我们所有的命令启动时,父进程都是bash
ls pwd top touch等命令都是进程,父进程全都是bash
代码创建子进程的方式
怎么做到创建一个子进程的呢?
fork
fork是一个系统调用,作用是创建子进程。
我们将代码写成这样:
fork之后有了两个执行流且都会执行后面的代码
果然:
其实一般是父进程的PCB拷贝一份给子进程,所以它们的PCB里有很多属性是一样的,但也有部分值不同。
子进程默认也会指向父进程的数据和代码
所以子进程在被调度的时候就会执行父进程之后的代码
子进程没有自己独立的代码和数据,因为目前没有程序新加载
子进程不会再执行fork之前的代码
fork的返回值
fork有两个返回值!把子进程pid返回给父进程,0返回给子进程
父子未来执行不同的代码逻辑,不要干一样的事,怎么改写?
第3点以后再说,先学前两个。
1.因为父进程:子进程=1:n
任何一个父进程可以有0、1个或多个孩子,任何一个子进程则只有一个父亲。
因为一个父进程有多个子进程,所以要把子进程的pid返回给父进程,父进程要用不同的pid来区分子进程。而子进程不需要获得父进程pid因为getppid就能获得。所以子进程只要看自己是否成功建立就行了。
2.为什么一个函数会返回两次?
一个函数如果已经到return了,核心功能已经做完了
fork本质是个系统调用
在走到return语句之前,子进程已经被创建甚至调度了,所以return是一条语句那么父子进程都会执行。所以就有两个返回值。
3.这一点先说一半
我们说子进程会继承父进程的代码,那么数据也是共享的吗?
在现实中,我们一个进程挂掉了并不会影响另一个进程。结论:进程具有独立性。
所以就算父进程挂了,子进程也会活得好好的。
这是因为它们的内核数据结构是独立的。
代码是只读的父子不能修改,所以也不会互相影响。
父子在数据上默认是共享的,但是如果父子一方要修改数据,OS把被修改的数据在底层拷贝一份,让目标进程修改这个拷贝。
这叫写时拷贝。