
目录
如果你在大学上过程序设计的课,那么这个人的名字你一定不会陌生——冯·诺依曼。
约翰·冯·诺依曼(John von Neumann,1903年12月28日-1957年2月8日),美籍匈牙利数学家、计算机科学家、物理学家,是20世纪最重要的数学家之一。冯·诺依曼是罗兰大学数学博士,是现代计算机、博弈论、核武器和生化武器等领域内的科学全才之一,被后人称为“现代计算机之父”、“博弈论之父”。
1945年6月约翰·冯·诺依曼与戈德斯坦、勃克斯等人,联名发表了一篇长达101页纸的报告《First Draft of a Report on the EDVAC》,即计算机史上著名的“101页报告”。在报告中冯·诺伊曼明确提出了计算机的体系架构,也就是现在著名的冯诺依曼体系结构。
从1951年第一台电子计算机EDVAC开始,计算机经历了多次的更新换代,不管是最原始的、还是最先进的计算机,使用的仍然是冯·诺依曼最初设计的计算机体系结构。因此冯·诺依曼被世界公认为“计算机之父”,他设计的计算机系统结构,称为“冯诺依曼体系结构”。
它包含输入设备、存储器、CPU(中央处理器)、输出设备

在计算机中,设备访问处理数据的速度是不同的,遵循:CPU >> 储存器 >> 外设
CPU的读取数据的速度很快,消耗的时间数量级在几纳秒(十的负九次方秒),而内存访问大概在百纳秒,磁盘访问消耗时间的数量级在几十毫秒。磁盘的读写速度比内存慢了几万倍,如果我们只是让CPU从磁盘读取数据,那么磁盘传输数据的速度远远小于CPU处理的速度,CPU在大部分时间都会闲置。
就如同,盖房子时垒墙的人一秒钟砌好一块砖,而递砖的需要一万秒才能给你递上一块砖,分别对应CPU和磁盘。
所以设计了内存用来弥补磁盘与CPU处理速度的巨大鸿沟,磁盘将数据传递到内存,内存再将数据传递给CPU,这样的分级数据传输让数据不断地流向快速处理数据的计算机组件,让计算机的运行效率大大提高。
到这里我们意识到,CPU直接与内存交互而不与磁盘交互。
存储器负责从外设读取数据来交给CPU,CPU将数据储存在寄存器内,CPU处理数据后,数据又会通过内存再写入外设。
CPU的执行速度很快,但是很笨。只能接收其他设备的指示,用别人的数据,计算别人的数据再将数据传回到储存器。
CPU接收到的指示就是我们写的代码,我们的代码保存在磁盘上,经过编译链接后会形成可执行程序。只有可执行程序,也就是二进制的机器码从磁盘上加载到内存上,再内存加载到CPU后才能开始执行。也就是说CPU只能运行自己可以识别的指令。
我们之前讲Linux时提到了操作系统的Kernel和Shell,Shell把我们的指令翻译成Linux能够理解的指令,然后操作系统,也就是核心Kernel会进行处理,然后讲结果传回Shell。CPU同样,它有自己的指令集,程序翻译过来的二进制机器码都是CPU可以看懂的指令集组成的。
假设张三要给李四发送一句你好(不考虑网络的具体工作情况),则两台电脑的数据流向:
张三电脑的数据流向:
(1)张三从键盘上输入你好,此时键盘这个外设上就有了数据。
(2)外设上的数据传递给了存储器,存储器就让CPU来处理这些数据。
(3)CPU从存储器上读取数据并且进行处理,然后再将处理后的数据写到存储器上。
(4)存储器再将数据写到网卡,网卡也是一个外设。
数据经过网络传递到李四那里
李四电脑的数据流向:
(1)李四电脑的网卡外设上受到了数据。
(2)外设上的数据传送给了存储器,存储器让CPU来处理这些数据。
(3)CPU从存储器上读取数据并且进行处理,然后再将处理后的数据写到存储器上。
(4)存储器再将数据写到显示器这个外设上。
首先给出操作系统的定义:操作系统是一个进行软硬件资源管理的软件。
你平时在使用电脑的时候,如果你去看你的任务管理器,你就算什么软件都不开,也会有大概一百个进程同时在运行,每一个进程又会传递数据给CPU,CPU又会将结果传回给程序。
这些进程的运行就需要操作系统的管理,可知操作系统可以管理软件。
你的显示屏、键盘、鼠标等这些铁疙瘩为什么能正常使用,这些都需要操作系统的有效管理才能实现。
这些设备的使用就需要操作系统的管理,可知操作系统可以管理硬件。
所以,操作系统存在的意义:通过合理管理软硬件资源,为用户提供良好的执行环境。
操作系统是一个管理者,他管理的内容本质上就是各种数据。
现实生活中也是如此,就比如在学校,校长是学校的管理者,二我们除了开学和毕业典礼大概率见不到校长。但是,如果你不好好学习,各种挂科,校长也是可以开除你的。校长没有见过你,但是却可以管理你,实质上就是通过我们的学号,姓名,电话,成绩等等信息来了解你现在的状况,进而管理你的。
而校长是通过辅导员,院级主任等各种渠道获得你的数据的,这些在你和校长中间的人都是中间执行者。这些中间执行者在管理硬件时就是驱动,操作系统管理硬件信息,指挥驱动运行对应的硬件。
首先对于硬件:
操作系统将使用不同类型的结构体对象将这些硬件的信息保存起来,比如说对应键盘就创建一个保存键盘信息的结构体,鼠标就就创建一个保存鼠标信息的结构体,最后将所有的结构体对象使用链表等数据结构管理起来。
软件资源也是类似的:
在操作系统中,软件的运行都可以看作一个或多个进程,每一个进程都对应创建一个名为tast_struct的结构体,最后将所有的结构体使用链表等数据结构管理起来。
操作系统这个管理者管理的内容主要有四大块:进程管理,文件管理,内存管理,驱动管理。
结论: 系统在管理资源的时候,先描述,再组织。
不过对于Linux操作系统是用C语言写的,所以描述进程都是用结构体来保存资源的属性,然后用链表或者其他高效的数据结构组织起来,方便管理。一般而言,使用的都是链表。
系统调用:系统提供的接口称为系统调用接口,俗称系统调用。
操作系统的管理保障计算机中软硬件的正常运行,用户才能正常使用计算机。如果用户直接与操作系统进行交互,对于操作系统来说非常不安全,如果用户在操作系统中乱操作,会导致系统崩溃。对于用户来说,需要对操作系统非常深入的了解才能够直接操作它,而且操作系统也不便于直接操作。但是,有时候我们还是需要获取操作系统中的一些信息,最简单的就是任务管理器,查看内存占用情况等。
于是操作系统提供了一些调用接口来供用户使用,这些接口和我们平时调用的函数差不多,只是此时它是系统接口,操作的是系统。这些系统调用相当于对操作系统的一些功能的封装,就如同哦我们去银行取钱,我们会通过柜台或者ATM机,而不是直接到银行的金库里直接拿走现金。
在开发角度,操作系统对外会表现为一个整体,但是会暴露自己的部分接口,供上层开发使用,这部分由操作系统提供的接口,叫做系统调用。操作系统给你一个安全的渠道去获取操作系统内部的信息,以免干扰自身的工作。
上图是计算机的软硬件体系结构示意图。

图中可以看到,用户可以通过shell外壳进行系统调用,比如pwd指令,在屏幕上打印出当前在哪个目录下。一些库函数,比如printf,都是通过系统调用来与硬件进行交互。printf能把内容打印到屏幕上也是应用了系统调用才能让程序与硬件交互。
系统调用在使用上,功能比较基础,对用户的要求相对也比较高。所以,有心的开发者可以对部分系统调用再次进行适度的封装,从而形成库,库内部的函数就是库函数,库的出现有利于上层用户的使用与操作系统本身的保护。
进程的概念:正在执行的程序。
程序源文件,在经过编译链接后会生成可执行程序,但此时可执行程序是存放在磁盘也就是外设上的。而程序要运行的时,可执行程序会从硬盘加载到内存上,就是将磁盘上的内容拷贝到内存中。
在可执行程序加载到内存中时,操作系统就会创建一个名称为task_struct的结构体,来描述可执行程序的各种信息,并且将这个结构体对象放入操作系统维护的数据结构中。
当可执行程序加载到内存中,对应的结构体对象也被插入到相应的数据结构中后,此时磁盘中的程序文件就被加载到内存中变成了一个进程。
所以说,进程 = 内核数据结构 + 对应的可执行程序代码。
同样其他的可执行程序被加载到内存中,操作系统也会一一对应地创建结构体对象,并且把它们都插入到相应的数据结构中来管理。每一个进程都需要经过CPU的处理,CPU执行进程的指令时,CPU会根据进程对应的结构体对象的内容,找到已经被加载到内存中的可执行程序去执行。
进程是动态的,具有动态属性。
操作系统通过管理进程对应的结构体对象达到管理进程的目的。
概念:用来描述进程信息的数据结构,可以理解为进程属性的集合。
用来描述进程各种属性进行的结构体,在Linux操作系统下这个结构体名字是task_struct,PCB是存储进程数据的结构体的统称,因为在其他系统中可能描述进程的结构体名称就不再是task_struct,但是起到的作用一致。
结构体的代码如下(部分,全部代码有两百多行):

不管它有多长,一定会包含以下信息:
- struct task_struct
- {
- //进程的各种属性
- //进程对应的代码位置
- struct task_struct* next//下一个结构体节点的地址,用链表串联起来
- }
每一个进程对应的结构体对象通过链表连接起来,每一个节点就是一个PCB,而在Linux中是这个PCB是task_struct
tast_struct结构体中的内容大致有如下几类:
这些具体的内容不需要详细了解,这里只是认识一下就可以了。
通过ps ajx查看正在运行的所有进程
指令:ps ajx
功能:显示运行中的进程的简要信息

我们查看进程信息时,第二行最上面有一个PID,这个PID值可以看作进程的编号。PID值不同,进程就不同;PID值相同,一定是一个进程。可以说,PID是一个进程的唯一标识符,是用来识别一个进程的。
ps ajx也可以使用管道组合其他指令来查看指定进程的信息
指令:ps ajx | grep 进程名
功能:查看筛选出来的进程的信息。
我输入了ps ajx | bash ,所有含bash的进程就被打印出来了

你也可以通过逻辑运算符同时查找多个对应名字的进程
指令:ps ajx | head -数字 && ps ajx | grep 程序名
功能:将多个指令组合在一起,显示出进程的信息,其中ps ajx | head -数字表示打印出打印所有元素的列表的前三行的内容
比如说,ps ajx | head -3 && ps ajx | grep bash,就相当于先打印出打印所有元素的列表的前三行的内容,然后再打印含有bash的进程,两个部分相互独立,执行完一个再执行另一个。

还有一种方法来查看进程,了解即可。
指令:ls /proc
功能:查看系统上当前运行的进程,proc是专门用来放进程信息的文件。

首先,我们右键单击Shell的对话框,点击复制会话。
然后,把新的会话拖动到右侧,如下图所示即可

我们在test.c中编写一段代码,然后编译运行。

我们就创建了一个进程./test,我们用Ctrl+c终止程序的运行,此时这个进程也消失了。
指令:Ctrl + C
功能:结束了一个正在运行的前台进程
Ctrl+c让此时正在运行的进程结束,但只有在前台运行的进程才能用这种方法终止。

指令:kill -9 进程PID
功能:杀掉PID值对应的进程
Ctrl+c可以终止前台进程,但kill -9可以终止后台和前台的进程,我们再次复制一个对话窗口并在左侧窗口运行一个test的可执行程序

程序运行时我们在右侧窗口查找相应的进程,其中第二个就是我们现在运行的进程

前面的一串数字前两个分别对应PPID(当前进程父进程的编号)和PID(当前进程的编号),当我们需要杀掉一个进程时输入的是PID,也就是第二个。

我们可以看到左边的程序也显示killed并停止运行了

我们通常用kill -19暂停一个正在运行的进程
指令:kill -19 进程PID
功能:暂停一个正在运行的进程
运行test可执行程序,在左侧查找test进程

输入指令暂停进程,下面就提示暂停了

在下面我们可以输入Linux指令

指令:kill -18 进程PID
功能:继续运行一个被暂停的进程
我们输入指令让其继续运行

此时进程也开始继续运行

后台程序基本上不和用户交互,优先级别低。前台的程序和用户交互,需要较高的响应速度,优先级别高。
在Linux系统中前后台程序的可以通过中间的进程状态判断,也就是1002数字前面的字母S,我们目前不需要知道S表示什么状态。只需要知道,字母后面有加号的是前台进程,没有加号的是后台进程。

前面说过,Ctrl+c不可停止后台运行的进程,而使用kill -9既能停止的前台进程,也可以停止后台程序。我们可以做个小实验。

上一个例子中暂停后再次开始运行是已经变成了一个后台程序,此时我们在左侧的对话框内输入Ctrl+c已经不再管用,只能输入kill -9才能终止

系统调用接口:getpid()
头文件:sys/types.h
功能:获得当前进程的PID
系统调用接口:getppid()
头文件:sys/types.h
功能:获取当前进程父进程的PID
在代码中可以使用系统调用获得当前进程的PID和父进程的PID,进程在运行时就会打印出当前进程的PID和父进程的PID。PID值是一个整型数据,使用%d打印即可
- #include
- #include
- #include
- int main()
- {
- int i = 0;
- while(1)
- {
- printf("我是一个进程,我的PID是:%d,我的父进程的PID是:%d\n", getpid(), getppid());
- i++;
- sleep(1);
- }
- return 0;
- }
这段代码会不断打印自己的PID和它的父进程的PID,每次打印睡眠一秒。我们在左侧会话运行这个程序,这个程序就变成了一个进程,右侧查找进程,发现的确获取了PID

我们终止掉这个进程,再次运行这段代码

在再次运行代码时我们发现进程PID的值变了,由32387变成了847,但父进程的PID没有变化。
当一个进程结束以后,操作系统就会将它的PCB删除,此时内存中就没有这个进程的信息了。我们再运行代码时,代码会再次加载到内存中,操作系统会创建新的PCB来管理这个运行的进程,也就是说这是一个新的进程,原本PID当然会改变。
那么这个不变的父进程是谁呢?我们可以通过父进程的PID查找。

此时我们找到了一个进程,名字叫bash,说明我们刚才运行的进程都是由bash产生的。这个bash就是我们之前讲到的shell外壳,也就是我们与操作系统交互的中介,它也是一个进程,我们执行的指令,运行的代码基本上父进程都是它。
系统调用接口:fork()
头文件:unistd.h
功能:在执行完fork以后,存在两个进程,一个父进程一个子进程。
通过man指令来查看fork系统调用

fork是用来创建一个子进程的,fork很像我们C语言中的函数,但是仔细阅读你会发现,fork的返回值有两个。
子进程创建成功,fork的返回值有两个,一个是子进程的PID,这个值会给父进程,还有一个值是0,这个值给子进程。创建失败,直接返回-1给父进程。
除了Python的函数可以有两个返回值,C、C++还是Java等主流编程语言的函数返回值只能有一个,如果向返回多个值,可以返回一个值并把其他的值通过传递指针和引用的方式带出函数。
我们运行下面这段代码
- #include
- #include
- #include
- int main()
- {
- int num = 1;
- pid_t id = fork();
- if (id < 0)
- {
- perror("进程创建失败\n");
- return 1;
- }
- else if (id == 0)
- {
- int i = 1;
- while(1)
- {
- printf("我是子进程,我的PID是:%d,我的父进程的PID是:%d,%d", getpid(), getppid(), i);
- i++;
- sleep(1);
- }
- }
- else if (id > 0)
- {
- int j = 1;
- while(1)
- {
- printf("我是父进程,我的PID是:%d,我的父进程的PID是:%d,%d", getpid(), getppid(), j);
- j++;
- sleep(1);
- }
- }
- return 0;
- }
运行后我们发现,父进程和子进程都打印出来自己的PID和父进程的PID

对于一个分支语句而言,只能进入其中一个语句框内,而这里却同时打印了id==0和id>0的内容,再编程语言中这样的情况是不可能发生的。
对于这个极其怪异的现象,我们可以这样理解:fork之后的代码是父进程和子进程共享的。
fork运行之后,子进程被创建,父进程也是同时运行。由于fork有两个返回值,子进程的PID值传递给父进程,是一个大于0的数,进入id>0的分支语句中。将0传递给创建的子进程,进入id==0的分支语句中,这样子进程与父进程才能同时打印。
