操作系统和进程的介绍

  • A+
所属分类:Linux

大家好!前面的文章讲了Linux下的基本指令和基本工具。现在我们正式讲解一下Linux的系统编程的知识。
操作系统和进程的介绍

1. 冯诺依曼体系结构

我们常见的计算机,如笔记本。我们不常见的计算机,如服务器,大部分都遵守冯诺依曼体系。
操作系统和进程的介绍
截至目前,我们所认识的计算机,都是有一个个的硬件组件组成。
输入设备:键盘,鼠标,扫描仪,话筒,磁盘等
输出设备:显示器,音响等
CPU(运算器+控制器):算数计算+逻辑计算和协调步骤
存储器:就是内存
为什么要有内存呢
1. 技术角度
我们知道:CPU的运算速度>寄存器的存取速度>L1~L3Cache>内存>>外设(磁盘)>>光盘磁带
如果我们没有内存,那么外设和我们的CPU直接联系。根据木桶原理:说任何一个组织,可能面临的一个共同问题,即构成组织的各个部分往往是优劣不齐的,而劣势部分往往决定整个组织的水平。导致计算机的运算速度由外设来决定,这就会导致计算机的运算速度很慢。所以我们加入内存,就会在数据层面上让外设不和CPU直接交互,而是和内存交互,CPU也是如此。那么内存在我们看来,就是体系结构的一个大的缓冲,适配外设和CPU速度不均匀问题。
2. 成本角度
能用较低钱的成本,获得较高的性能。
所以内存是干什么的呢
它用于暂时存放CPU中的运算数据,以及与硬盘等外部存储器交换的数据。

对冯诺依曼的理解,不能停留在概念上,要深入到对软件数据流理解上,请解释,从你登录上qq和某位朋友聊天开始,数据的流动过程
第一步,我们在键盘上(输入设备)输入你好,那么数据就会到存储器里,然后CPU会从存储器里取数据,然后运算处理,处理好后刷新到存储器里。然后在从网卡(输出设备)传递给你朋友的计算机的网卡(输入设备)接受,然后放到存储器里,然后由你朋友的计算机的CPU进行处理然后在刷新到存储器里,最后由显示器(输出设备)来显示。

2. 操作系统

2.1 概念

任何计算机系统都包含一个基本的程序集合,称为操作系统(OS)。笼统的理解,操作系统包括:
内核(进程管理,内存管理,文件管理,驱动管理)
其他程序(例如函数库,shell程序等等)

设计OS的目的:
与硬件交互,管理所有的软硬件资源。为用户程序(应用程序)提供一个良好的执行环境。

定位:
在整个计算机软硬件架构中,操作系统的定位是:一款纯正的“搞管理”的软件

2.2 如何理解 “管理”

举个生活的例子:在学校里,谁是最大的管理者呢?一定是校长。那么校长都不认识我们,是如何管理我们的呢?原因是:校长有我们学生的数据,比如姓名,年龄,专业,成绩等等。所以他是根据我们的数据,来做出相应的决策管理我们。管理的本质:不是对被管理对象直接管理,而是对被管理对象的数据进行管理校长有了我们全校学生的数据,就会抽取所有同学的属性,描述对应的同学。然后可以用链表这样的数据结构给学生管理起来。如果我们想按照成绩排名,就可以对这个结构排序,这样就起到了管理的作用。也就从数据的管理转变为对某种数据结构的管理
结论:管理的核心理念:先描述,再组织。

2.3 系统调用和库函数概念

下面图片是操作系统的具体管理结构:
操作系统和进程的介绍
而下面的三部分组成了操作系统的体现结构。
既然知道了操作系统的体系结构。那么现在有一个问题:操作系统为什么要给用户提供服务呢
像我们平时写的printf和cout函数,可以通过这些函数的调用来打印到显示器上。所谓的打印,本质就是将数据写到硬件上。但像我们写的C/C++程序,有资格向硬件写入吗?答案是:没有资格的。它其实是通过操作系统来帮我们完成任务。所以操作系统设计出来就是为了给人提供服务的。

操作系统是如何给我们提供服务的呢
因为为了防止某些人可能有意或无意的修改了我们的操作系统,所以操作系统是不会直接暴露自己的任何数据结构,代码逻辑和其它数据相关的细节!但是操作系统可以通过系统调用的方式,对外提供接口服务。由于Linux操作系统是用C语言写的,这里的"接口",本质就是C函数

但是同学们有没有想过一个问题:就是这个系统调用,我们作为一个初学者,去直接使用的成本太高。所以它会给我们提供一些图形化界面或命令行解释器的方式去使用,从而间接的系统调用。就像我们写C语言时的printf时,它会去链接lib,然后通过系统调用(但是你不知道)打印到显示器(硬件)上。

那么还有一个问题:Linux下的系统接口和windows的系统接口一样吗?是不一样的。但是printf函数在Windows和Linux下实现的结果是一样的,它在Linux下调用Linux的系统接口,在windows下调用windows的系统接口。这就是说C语言是可移植的,可跨平台的原因。

总结:
1.在开发角度,操作系统对外会表现为一个整体,但是会暴露自己的部分接口,供上层开发使用,这部分由操作系统提供的接口,叫做系统调用。
2.系统调用在使用上,功能比较基础,对用户的要求相对也比较高,所以,有心的开发者可以对部分系统调用进行适度封装,从而形成库,有了库,就很有利于更上层用户或者开发者进行二次开发

3. 进程

3.1 概念

在许多书上写进程的概念是:进程是一个运行起来的程序。这句话是不严谨的。
解释一下:
假设,我们现在有个CPU,有个内存,有个磁盘,在磁盘上有个文件test.c,然后这个test.c文件编译链接成一个可执行程序test.exe。
在这里插入图片描述
那么可执行程序是文件吗?答案是:是的。因为文件在磁盘里。那么现在我们运行这个程序,把这个test.exe文件加载到内存中。
在这里插入图片描述
此时的test.exe就叫做进程吗?不是的,就像学校食堂的厨师他是学生吗?
虽然现在我们不知道进程到底是什么,但是我们知道操作系统中存在大量的进程。
操作系统和进程的介绍
那么操作系统就需要将所有的进程管理起来。那么根据前面讲的"管理",对进程的管理,本质就是对进程数据的管理。所以我们需要先描述,再组织。

3.1.1 描述进程-PCB

那么如何去描述一个进程呢?Linux的内核是用C语言写的,所以会有一个struct的结构体来描述进程的所有属性。
操作系统和进程的介绍
每一个内存中的可执行程序都会有结构体来描述,然后在内存中这些结构体会被组织成一个数据结构来管理。而这个结构体在课本上称之为做进程控制块(PCB—process control block),Linux操作系统下的PCB是: task_struct。

而真正进程的定义其实是内存中的可执行程序代码和对应的结构体一起被称为进程
操作系统和进程的介绍

3.2 如何查看进程

先完成准备工作:
操作系统和进程的介绍
操作系统和进程的介绍
操作系统和进程的介绍
此时我们运行后,可以说它是一个进程了。
操作系统和进程的介绍
那么现在我们该如何查看这个进程的相关信息呢?我们可以使用这个命令:
操作系统和进程的介绍
第一个就是我们运行起来的进程信息。那么第二个又是什么呢?其实像我们用的命令,当我们去使用它时,它也算是一个进程,所以第二个进程就是我们所使用的命令。

如果我们想屏蔽第二个进程,我们可以这样做:
操作系统和进程的介绍
这是我们第一种查看进程的方法,也是我们以后最常用的一种

那么还有第二种查看方式:
操作系统和进程的介绍
我们可以看到,在根路径下存在许多的目录。而有一个目录叫做proc,这个目录叫做:内存文件系统里面存放的是当前系统实时的进程信息
现在我们来看一下proc目录里面到底存放了哪些进程呢?
操作系统和进程的介绍
这里大家一看发现看不懂。现在我就教大家这些数字的含义到底是什么?
操作系统和进程的介绍
这里我们将进程的每一个属性的意义打印出来。或者我们可以这样:
操作系统和进程的介绍
此时我们可以看到一个PID的属性:其实每一个进程在系统中,都会存在唯一的标识符(pid->process id)。就像每一个同学都有自己的学号。
那么现在我们就可以在proc目录下去寻找了:
操作系统和进程的介绍
我们看到确实有这个目录。那么我们现在把这个进程结束了。
操作系统和进程的介绍
操作系统和进程的介绍
就找不到这个目录了。所以这也体现了它确实是记录实时的进程。现在我们再给它运行起来:
操作系统和进程的介绍
从这里我们可以看到其实每次重新的一个进程,它的PID是会重新分配的。
操作系统和进程的介绍
现在,在这个目录下,我们首先看这两个东西。cwd:进程当前的工作路径。exe:进程对应的可执行程序的磁盘文件。

现在就出现了一个问题:当前路径是什么意思
操作系统和进程的介绍
这个fopen函数如果没有tmp.txt文件,那么就会在当前路径下去创建一个tmp.txt文件。如果不懂的可以去看一下我的C语言文件哪里的内容。C语言文件通道
操作系统和进程的介绍
我们看到创建成功了。那么当前路径下的意思是源代码的路径吗?我们在来看:
操作系统和进程的介绍
所以当前路径真正的意思是:当前进程的所在路径,不是源代码的路径

那么像pid,当前路径这些数据在哪里呢?这些进程的内部属性其实就在进程的进程控制块(PCB)结构体中

3.3 通过系统调用获取进程标示符

上面我们是通过指令来得到进程标示符的,作为一名程序员我们该如何通过系统调用的方法来获取pid呢?
我们可以通过一个getpid函数,首先,我们先man 2 getpid进行查看:
操作系统和进程的介绍
我们需要用这个系统调用,我们需要引入这两个头文件,这里的pid_t的意思是无符号整型的意思。
操作系统和进程的介绍
这里是C语言的接口吗?不是的,这是系统调用的接口,你在Windows下不一定能这样去使用。
操作系统和进程的介绍
那么现在程序已经运行起来了。我们想结束这个进程该怎么办呢?之前我们用的都是Ctrl+c这个热键。现在教大家一个命令:kill -9 pid
操作系统和进程的介绍

3.3.1 父进程id(PPID)

如何获取ppid呢?我们可以使用getppid()函数。
操作系统和进程的介绍
操作系统和进程的介绍
我们可以看到pid和ppid都有了。但是父进程为什么不变?它是谁呢
操作系统和进程的介绍
在这上面有许多的21395,但我们只看pid为21395的就行。那么它就是bash。所以几乎我们在命令行上执行的所有的指令(你的cmd),都是bash进程的子进程

3.4 通过系统调用创建进程

创建进程有许多方法,第一种就是./你的程序。那么能不能系统调用的方式来创建进程呢?我们可以使用fork,那么我们可以man 2 fork 来看一下这个函数。
操作系统和进程的介绍
然后我们再来看一下它的返回值。
操作系统和进程的介绍
这个返回值的意思是:如果成功了,父进程返回子进程的pid,子进程返回0是不是觉得很奇怪,有两个返回值。那么我们就需要验证一下:
操作系统和进程的介绍
我们创建了一个test.c文件,里面使用了fork函数。从上面的返回值我们可以得出使用这个函数后,会出现两个进程。我们来看一下运行结果:
操作系统和进程的介绍
从结果上看,它打印了两次。我们在做一下改动:
操作系统和进程的介绍
运行结果如下:
操作系统和进程的介绍
同一个id值,只使用了打印,没有修改,却打出来了不同的值。现在这个问题我们还回到不了,后面再说。

现在我们再来看一个例子:
操作系统和进程的介绍
可能大家看到这个代码非常的奇怪。C语言上if和else if是不可以同时执行的,也不可能两个死循环同时运行。那么我们来看运行结果:
操作系统和进程的介绍
从运行结果我们可以得出结论:
fork之后,父进程和子进程会共享代码,一般都会执行后续代码。
fork之后,父进程和子进程返回值不同,可以通过不同的返回值,判断,让父子执行不同的代码块

现在可能大家有许多问题:第一个问题:为什么父进程返回子进程的pid,子进程的pid为0?
我们都知道一个父亲他可以有许多孩子,但孩子只有一个父亲。那么父亲就会给每个孩子起名来标识孩子们。所以,父进程必须要有标识子进程的方案,fork之后,给父进程返回子进程的pid而子进程最重要的是要知道自己被创建成功了,因为子进程找父进程成本非常低(getppid())。

第二个问题:fork如何做到有不同的返回值?
首先,我们知道fork()函数的调用是系统接口提供的。就像C语言的printf函数它在C标准库中有它的定义。那么fork函数在操作系统中也一定有它的定义。
操作系统和进程的介绍
那么fork之后,OS做了什么?是不是系统多了一个进程。它们分别是:
task_struct+进程代码和数据。
task_struct+子进程的代码和数据。

那么子进程的task_struct对象内部的数据从哪里来的呢
答案是:基本是从父进程继承下来的,也有属于自己的数据。

那么子进程是干什么的呢
子进程是执行代码,计算数据的。
那么子进程的代码是从哪里来的呢
是和父进程执行同样的代码,fork之后,父子进程代码共享(但数据各自独立),不同的返回值,让不同的进程执行不同的代码逻辑。

那我们继续下一个阶段:
操作系统和进程的介绍
第一个问题:调用一个函数,当这个函数准备return的时候,这个函数的核心功能完成了吗?答案是:已经完成了。既然完成了,那么说明子进程已经被创建成功了。

第二个问题:如何理解进程被运行?
我们知道进程的PCB可以放在双链表中,其实也可以放其它的数据结构中。在操作系统中有一个代码模块叫做调度器。在Linux内核中,每一个CPU都有一个叫做运行队列(用链表实现)的东西,而里面是tack_struct。每一个tack_struct都有对应的代码和数据。
在这里插入图片描述
假如调度器调度第一个tack_struct时,CPU会去找到它的代码,然后访问它的数据。
在这里插入图片描述
那么子进程会和父进程共享同一块代码,数据暂时不考虑。然后把子进程放到运行队列中,给CPU调度。
操作系统和进程的介绍
所以,当执行到return pid的时候,已经完成了两个任务:
1.子进程已经被创建了。2.将子进程放到了运行队列中。
所以,return pid这个代码也会被两个进程所共享,所以会出现两个返回值。

w3cjava

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: