前言
一. 了解冯诺依曼系统

我们认识的计算机都是由硬件组成的,像键盘,鼠标,磁盘,网卡等都属于输入设备,显示器、打印机,网卡,磁盘都属于输出设备,存储器就直接当成内存来看就可以,CPU就是运算器加控制器。
我们都知道软件运行,需要先加载,那是把这个程序的数据先加载到哪里,又或者说程序运行,通过CPU执行我们的代码,访问数据之前,这个程序在哪里呢?看这个图就明白,其实是在内存。也就是说,程序也是文件,文件一开始都在磁盘,文件数据先是通过input,将其“拷贝”到内存,再将内存的数据“拷贝”给CPU,CPU对我们的代码执行,访问数据后,又“拷贝”到内存,最后再把结果通过output“拷贝”到输出设备。那我们可以知道了:原来冯诺依曼体系结构的效率是由“拷贝”的效率决定的。这里再举个例子:假设我在北京要给远在南京的朋友通过QQ发送一条短信,计算机是怎么实现这个过程的呢?
首先,我们要用QQ,是不是得先开启,一开启不就加载到内存了吗(这个跟刚刚讲的一样,CPU访问数据之前,数据都是加载到内存)我先从键盘(输入设备)输入信息,之后这些信息被拷贝到内存,CPU获取内存数据对其进行处理后拷贝给我的网卡和显示器(输出设备),然后这些数据就通过网络传送到朋友的网卡(输入设备),同理拷贝到朋友电脑的内存后经过CPU的处理将信息拷贝到朋友电脑的显示器或者加载到磁盘中,这就是用两个冯诺依曼体系实现了数据的流动。发送文件也是一样的过程。
观察这张图可以发现(我们只考虑数据信号,就是红色箭头),在数据层面,CPU只能对内存进行读写,输入或输出设备要想输入输出数据也只能写入内存或从内存读取(这里说的输入输出和写入读取都是站在内存的角度来描述的),可以得出结论:所有的设备都只能直接和内存打交道。
二. 操作系统的概念和定位
任何计算机系统都包含⼀个基本的程序集合,称为操作系统(OS)。这么说有点抽象,其实操作系统就是一款进行软硬件管理的软件,它自己就是软件。笼统的理解,操作系统包括:
内核(进程管理,内存管理,⽂件管理,驱动管理)--狭义上的操作系统
其他程序(例如函数库,shell程序等等)
我们接下讲的都是狭义上的操作系统,为什么要设计操作系统呢?
观察这张图我们可以发现:
1.这是一张软硬件体系层状结构,特点:高内聚低耦合,这个与C++类的继承相似。
2.访问操作系统,必须使用系统调用,这些接口其实就是函数,是系统提供的函数。
3.我们的程序,只要是访问了硬件,那么就必须从用户这一层一直向下,贯穿整个软硬件体系结构。
4.我们之前写代码用的C标准库(libc),其实是在用户操作接口这一层。这一层为用户(包括开发者)提供了与操作系统交互的接口,使得用户可以通过编写程序来执行各种操作,也可以看出库可能在底层封装了系统调用。
这种结构对下可以实现:操作系统与硬件交互,管理所有的软硬件资源,不过这只是手段。真正的目的是向上----可以为用户程序提供良好的执行环境。
三. 操作系统的核心功能:
一款真正搞管理的软件,具体怎么体现呢?六个字,先描述,再组织。
举个例子,现在有校长、辅导员、学生三种角色,校长需要得到很多学生的信息:姓名、性别、年龄、籍贯、入学时间、成绩、宿舍号等,那么校长需要对每一个学生挨着问他们的信息吗,显然不是,是要通过对辅导员发号施令,让他们去执行。假如辅导员发布了一个在线文档让学生们填入各自数据,这样一来校长就可以得到一张excel表格,然后就可以查看哪个学生的成绩高哪个学生成绩低,这里校长是管理者,学生是被管理者。
1.要管理,管理者和被管理者,可以不需要见面
2.管理者怎么管理被管理者呢?根据“数据”进行管理!
3.不需要见面,如何得到数据?由中间层获取!
可是那么多学生,难道校长要每天起来两眼一睁就看这个表格谁的成绩高谁的成绩低吗?这显然是不切实际的,所以呢,聪明的校长就去学了c语言,他发现,可以用结构体把每个学生的不同信息(包括姓名、性别、年龄、籍贯、入学时间、成绩、宿舍号)给聚合起来,然后呢,张三李四王五每个学生都是一个结构体变量,可是呢,每个变量之间又是互相独立的,学生这么多,校长就想到用链表把每个学生的结构体数据给串起来,之后,校长就可以通过遍历链表查看来哪个学生的成绩最高,也可以在对挂了二十科的学生劝退后,在系统中删除学生的信息,就相当于把对excel表格的数据管理转化成对链表的“增删查改”。
这种校长管理学生的工作转换成对链表的增删查改其实就是建模的过程,(校长就是操作系统)就是先描述(结构体),再组织(链表结构),c++中类(把要描述的对象的各种属性聚合起来)和STL(各种各样的数据结构)的设计也是源于操作系统先描述再组织的特点。

还是这张图,操作系统是怎么管理底层硬件和驱动程序的呢?这里把操作系统看作校长,底层硬件看作学生,驱动程序看成辅导员。操作系统管理底层硬件,不需要直接沟通,而是通过驱动程序拿到硬件的各种数据,(先描述)每一种硬件在操作系统内都是数据结构对象,然后将所有对象用链表或者其它数据结构给串联起来(再组织),实现对硬件设备的管理。
四. 系统调用和库函数概念

如何理解系统调用呢?我们可以将操作系统看作银行,人去银行取钱是不能直接进入银行仓库取钱的,要通过柜台将钱拿给人,这里柜台就相当于系统调用,为什么需要系统调用,就跟银行是不信任人一样,操作系统是不信任用户的,但是它又要向上为用户或者开发者提供对应的服务,所以才需要系统调用,我们也可以把系统调用理解为上面这张图的C函数(这只是个假设),用户和操作系统进行某种数据交互时,用户需要将参数传给操作系统,操作系统需要把返回值传给用户。
结论:在开发角度,操作系统对外会表现为⼀个整体,但是会暴露自己的部分接口,供上层开发使用, 这部分由操作系统提供的接口,叫做系统调用。

那这个用户操作接口又怎么来理解呢?
假设现在有个大爷来取钱但是他不识字,不能直接通过柜台跟工作人员交流,那么这个时候大堂经理就可以帮助大爷填好各种信息,才能通过柜台跟工作人员拿钱。这里大堂经理就相当于用户操作接口。
结论:系统调用在使用上,功能比较基础,对用户的要求相对也比较高,所以,有心的开发者可以对部分系统调用进行适度封装,从而形成库,有了库,就很有利于更上层用户或者开发者进行二次开发。
我们大概可以明白,操作系统对底层硬件的管理是“先描述再组织”,那么它对进程的管理也一样,也是“先描述再组织”。
五. 进程
1. 基本概念:之前对进程可能有误解,认为程序运行之前会先加载到内存,把这里的代码和数据当成进程,其实不是。

当有多个可执行程序被加载到内存时,CPU在执行代码时不知道这些代码和数据对应哪个程序,所以操作系统必须对它们进行管理,此时操作系统就需要创建一个内核数据结构类型:struct xxxx对各个程序的属性进行描述,同时内部也有指针指向对应的代码和数据,这个类型也叫进程控制块(包含进程的所有属性)。在所有操作系统叫PCB(抽象概念),Linux是具体的,叫struct task_struct,就相当于shell 和bash的关系。描述完后需要用指针将各个进程连接起来(组织),形成一个队列,后面CPU在对进程进行调度时就可以根据各个进程的优先级顺序,执行对应的代码;也可以在程序执行完后将这个struct task_struct对象从队列中删除,对进程的管理就转换成了对链表的增删查改,这个跟前面在操作系统功能部分的举例类似。
结论:进程=内核数据结构对象+自己的代码和数据,在Linux里 进程=PCB(struct task_struct)+自己的代码和数据。甚至可以认为进程就是PCB。

举个例子再理解以下操作系统对进程的管理:假设你现在毕业了找工作,是不是要投简历,不仅你要投简历,许多毕业生也要投简历,这些简历就记录着你们的信息,hr把你们的简历一个个整理好放到面试官面前。这个过程,你就是代码和数据,你的简历就是PCB,hr就是操作系统,hr是对你的简历进行管理而不是你这个人,也就是说可执行程序加载到内存里,这个程序的代码和数据(人)本身不重要,重要的是os为它创建一个struct task_struct(简历),然后链入队列(在面试官桌子上整齐排放),等待CPU调度(等待面试官叫你问你问题,这里要澄清一点:操作系统对进程进行管理,但是当CPU调度进程时,需要通过内存指针来执行进程的代码和访问数据)。
2. task_ struct 内容分类
- 标示符:描述本进程的唯一标示符,用来区别其他进程。
- 状态:任务状态,退出代码,退出信号等。
- 优先级:相对于其他进程的优先级。
- 程序计数器:程序中即将被执行的下一条指令的地址。
- 内存指针:包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
- 上下文数据:进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器]。
- 1/0状态信息:包括显示的I/O请求,分配给进程的I/0设备和被进程使用的文件列表。
- 记账信息:可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
- 其他信息
- 具体详细信息后续会介绍
3. 查看进程信息
(1)我们历史上执行的所有指令,工具,自己的程序运行起来,都是进程!
(2)进程具有唯一性:进程id是唯一的,查看进程id接口:getpid() ,这是一个系统调用接口(因为pid是进程的信息,保存在task_struct里),返回值pid_t 就是 int ;哪个进程调用它,返回的就是哪个进程的ID值(一般大于0)。

(3)查看当前程序的进程信息方法:a.
这里我们可以看到进程的PID。
ps ajx | head -1 是显示进程列表的头行,包括ppid,pid等属性,第四行也有process的原因是grep启动时也是一个进程,后面也带了myprocess,所以这行显示的是grep这个进程,要想去掉这一行,再指令行后面添加 grep -v grep即可。
b. 
我们也可以通过 /proc 查看当前所有进程,proc是内存级的文件系统,os不仅可以让你通过ls指令查看磁盘里的文件,还可以动态访问内存的文件。
这里面的数字就是一个个的进程id,相当于一个目录,记录着指定进程的各种属性。这里的9指的是杀掉进程时可以用:kill -9 进程id / ctrl+c。
补充两个重要属性知识点:
- cwd: 当前进程的路径,当程序加载到内存,task_struct会记录自己的当前路径,这也是为什么我们使用 fopen函数建立一个文件时,不需要带指定路径,直接写文件名就好。如果想改变当前路径可以使用chdir()这个系统调用接口。

- exe :进程对应的可执行文件
(4)bash也是进程
在前面查找pid的函数里由一个函数 getppid() ,这个是用来查找当前进程的父进程的。当我的程序每次运行时,pid总是不一样,呈线性增长,但是父进程的pid却是一直一样的,这里的父进程是bash,也就是说bash这个命令行解释器本质也是一个进程,操作系统每次启动都会给每个用户分配一个bash。
4. 创建子进程:使用fork() 系统调用函数
子进程被创建,默认是拷贝了父进程的PCB,有些属性不同(比如pid,ppid),两者默认共享父进程的数据和代码。

这里举个例子观察创建子进程后代码被执行的次数跟之前C语言的时候有什么不同
程序运行时:

我们会发现,子进程被创建后,第11行的代码被执行了两次,一次被父进程执行一次被子进程执行,你可能会有很多疑惑,让我一一为你解答。

观察返回值发现:fork()返回给子进程是0,给父进程是子进程的pid
(1)为什么fork()给父子各自不同的返回值?父进程要通过不同的pid来区分子进程,未来方便对子进程做管理,而子进程已经可以通过getppid()得到父进程的pid,它只需要证明自己创建成功就可以所以返回0。
(2)为什么一个函数返回两次?先声明:一个函数已经到return 了,它的核心工作已经做完了。fork()也是一个函数,在代码执行到 if 前,函数内部在创建子进程:要向内存申请新的PCB,拷贝父进程的PCB给子进程,将子进程的PCB放入进程队列,甚至放入调度队列,也就是说在return之前子进程已经被创建好了,return id 是代码,代码是父子共享的,所以return被执行了两次(父和子)
(3)为什么id变量既大于零又等于零,使得ifelse同时成立呢?原因是进程具有独立性,数据结构task_struct父子进程各一份,代码和数据共享,代码是只读的不影响,当父子任何一方对数据如变量进行修改时,操作系统把被修改的数据在底层拷贝一份,让目标进程修改这份拷贝。这就是为什么子进程对val的值进行修改,而在父进程中val的值不变的原因。前面id有两个值也是因为发生了写时拷贝。(这里埋下一个问题,操作系统是怎么做到让一个变量可以有不同的值呢?)



1864

被折叠的 条评论
为什么被折叠?



