学进程前我们需要知道什么?
一、冯诺依曼体系结构
图中就是我们电脑运作时的大致工作流程,其中输入设备、输出设备我们也叫外设。其中,输入设备有比如键盘、鼠标、磁盘、摄像头等。输出设备有显示器、磁盘、打印机等。图中的存储器我们也叫内存,用黄色框起来的部分叫中央处理器也就是CPU。通过这个图片我们需要知道一个点:
CPU在数据层面,不会和外设直接打交道,只会和内存进行交互,也就是CPU想获取信息只能从内存部分提取而不能直接从外设获得。并且,任何程序,运行时都必须先被加载到内存然后进行处理。
这也是有我们的体系结构决定的。代码->可执行程序->运行->加载到内存。我们通俗一点来说,就是我们的输入设备获取我们要输入的信息,然后把文件交给内存,CPU获取内存里的文件并转换成机器可以识别的文件然后再还给内存再输出。这么看来,内存这个东西似乎有点多余,貌似我把内存去掉,直接输入->CPU->输出更便利些。但这样的设计其实并不合理,我们从速度来讲,外设的速度是远低于我们CPU的工作速度的,但由于木桶效应,我们实际的运行速度只会参考外设的速度作为实际速度,同时由于经济原因,如果我们想把外设换成高速配件又太贵,因此我们用内存作为桥梁,它的地位在中间,经济适中且速度不慢也不快,我们让外设和CPU分别和内存交互就可以提高速度了。
二、操作系统的基本概念
操作系统本质分为内核和其他程序,其中前者包含进程管理,内存管理,文件管理,驱动管理。后者也是操作系统不可或缺的一部分,包括函数库,shell程序等等。操作系统是一种进行软硬件资源管理的软件。
三、操作系统如何进行资源管理
管理的本质是对数据进行管理,从下图我们得知,操作系统向上通过管理数据向用户提供一个有效便利的工具进行操作,向下对数据进行有效管理。我们先向下看,研究一下操作系统是如何管理硬件资源的。
我们发现,操作系统和硬件部分并没有直接的联系方式,只能靠中间角色——驱动程序实现上对下的决策,下向上的数据获取。而我们的操作系统以决策为主,驱动部分以执行决策为主,这样就能获取数据了。
但具体细节是怎样呢?虽然每个硬件不一样,但其有大致相同的属性,操作系统用类似结构体封装的方法把每个硬件封装起来,然后如果有新的硬件加入,就可以通过驱动程序报告操作系统,然后操作系统再进行修改。
操作系统向下的管理我们基本了解了,接下来看一下它是如何向上运作便于用户使用呢?
其实就和银行的运作原理是类似的,我们到银行办理业务,正常来说要找业务员进行办理,但实际上,如果我们知道办公的原理,跳过业务员这一步自己办理也是可以的,但是这会大大增加风险,我们不能保证每个人都能顺理成章地完成自己的工作,难免会有失误,因此银行为了避免这种情况,直接封死了客户自行办公的途径,同时又开放了一个新的路径——业务员窗口办公,这样就可以了。我们换到操作系统层面,我们要想对硬件进行写入等操作,并不是直接对硬件进行操作,而是需要经过上图的整个流程,所以,操作系统必须向上提供开放各种接口,方便上层使用并保证下层体系的安全稳定——system call(系统调用接口)。
这部分的意义在哪里呢?其实,我们的红色部分的使用,需要我们对系统有一定了解的人才可以顺利地使用,比如程序员,像很多电脑小白连打字都不熟练自然也就不知道这些底层逻辑,所以为了迎合大部分人的需求,需要再封装一个用户调用接口,再封装成库、shell外壳等接口,让大多数用户也可以便利使用。直接访问库就可以了,就不需要直接访问操作系统了。比如C标准库/C++标准库,我们平时的printf、scanf函数就是在访问这些库接口。
————以上是我们对于计算机结构体系的一些初步认识,下面开始讲解重点内容——进程
四、什么是进程
大多数课本的概念是——程序的一个执行实例,正在执行的程序等。更深一步的概念我们可以称进程是内核数据结构+程序的代码和数据。我们来解释一下这个概念
首先我们写的程序或文件都会加载到磁盘里,其中操作系统为了方便管理进程,把磁盘中的程序放进内存(黑框转移到绿框,本质是代码和数据,也就说书本上指的进程),然后根据我们前面操作系统管理硬件的原理参考,操作系统管理进程的方法,也是创建结构体记录进程的属性,其中内部还需要包括进程在内存的指针(可以通过指针查 找进程)和其他进程的结构体指针(使得管理成为一个数据结构的增删查改),在课本上这个结构体成为PCB(process control block),在Linux中成为task_struct,我们要找到一个进程只需要找到其对应的结构体地址即可找到。需要运行时,进程就会先从磁盘进入内存,被操作系统记录管理后再把代码和数据交给CPU处理。这个PCB就是操作系统管理进程的方式。
进程也是运行起来的程序,会被根据task_struct属性被操作系统调度器调度运行。
五、Linux下的进程
1.如何查看进程信息
首先我们需要知道一个知识点:当我们启动一个程序,就相当于启动一个进程,当程序运行时,代表着该进程一直存在。所以就会出现两种进程,一种是刚运行就结束的进程,比如函数的调用等,另一种是长期进程,像杀毒软件,长期在后台运行。
在Linux中,我们想查看系统有哪些进程,可以用ls /proc指令查看,这是因为,我们的根目录下有一个名为proc的文件夹
文件夹里就记录着此刻进行的所有进程
我们发现,这里还有很多以数字为名的文件夹,其实这些数字代表着每一个进程的pid,可以理解为地址,当我们启动一个进程时会生成一个pid,此文件夹有这个pid说明此时这个pid的进程正在运行。因此这些数字文件夹的里面就记录着进程的属性等信息了。我们可以用getpid()函数来获取某个进程的pid。
我们发现,当我们程序运行时,的确能在/proc中找到对应的数字文件夹,当我们把程序终止后就找不到了。
此外,终止程序除了我们了解的ctrl+c,还有kill命令,只需要在另一个终端输入即可。
kill -9 pid(你想要终止程序的pid)
进入某一进程的信息我们发现其实信息属性还是蛮多的,其中cwd和exe是比较重要的,exe会记录当前程序是在哪一路径下运行的,cwd就是当前工作目录,就是进程的cwd。更改cwd的方法可以用chdir函数
chdir(目标路径)
2.ppid
ppid也就是父进程id,我们新创建任何进程的时候,都是由自己的父进程创建的(是父进程让操作系统创建)。我们可以用getppid函数获得父进程的id,我们在多次运行后发现,一个进程,它的pid是随机的,但ppid是一直不变的。这是因为,命令行中,执行命令/程序,本质是bash的进程创建的子进程,然后子进程来执行我们的代码。那么bash是如何创建新进程来执行程序的呢?
3.使用系统调用创建进程
刚才我们所新建的进程都是通过命令行输入指令实现的,如果不想通过这种方式采用系统调用接口来实现新建进程也可以,这个接口就是fork函数。
fork的功能是创建一个子进程,包含在unistd头文件中,其中fork的返回值是
一旦我们通过fork成功创建子进程,它就会返回子进程的pid给父进程,返回0给子进程,失败返回-1。我们来看下面的代码
我们发现,第二个printf函数好像被执行了两次,那为什么第一个printf只执行一次呢?
原因是,当我们开始写这个程序的时候,进程已经开始了,也就是第一个printf也是进程,第二个printf前因为有fork,执行分支又一条变成了两条,两个分支都要运行,所以第二个printf会跑两次。我们还发现第二行和第三行的pid差1,第一行和第二行的pid又相同,所以第一个分支就是父进程(自己的进程,第一行的ppid就是bash),由fork创建的新分支就是子进程,而且一个父进程可以有多个子进程,Linux的进程也是树形结构。
我们改一下代码验证一下返回值的问题
我们发现,两个while居然同时运行了,也就是if和else if同时成立,这和我们之前的了解不一样啊,我们来解释一下。因为有fork函数,在fork以后就会有两个进程同时运行了,这两个进程会各自执行各自的代码,也就是导致条件不同(因为父子进程返回值的差异)。
父进程的数据是从磁盘获取的,子进程的数据是从父进程继承过来的。但数据却各有一份,即使修改也不会影响对方。
六、多进程代码样例
创建多进程的思路不难,只需要循环调用fork函数即可,我们通过结果发现,每一个子进程的ppid都是父进程的pid,体现了父子的关联性,而每一个子进程的pid都是相邻的,体现了树形结构。