目录
冯诺依曼体系结构
操作系统(Operator System)
概念
设计OS的目的
定位
如何理解 "管理"
总结
系统调用和库函数概念
进程
基本概念
描述进程-PCB
task_struct-PCB的一种
task_ struct内容分类
组织进程
查看进程
通过系统调用获取进程标示符
通过系统调用创建进程-fork初识
进程状态(重点)
操作系统状态:
Linux操作系统状态:
Linxu操作系统非常特殊的一种状态——Z(zombie)-僵尸进程
僵尸进程危害
孤儿进程
进程优先级
基本概念
查看系统进程
PRI and NI
PRI vs NI
查看进程优先级的命令
其他概念(了解向)
冯诺依曼体系结构
我们常见的计算机,如笔记本。我们不常见的计算机,如服务器,大部分都遵守冯诺依曼体系。
截至目前,我们所认识的计算机,都是有一个个的硬件组件组成
- 输入单元:键盘,话筒,摄像头,磁盘,扫描仪,写板,网卡等
- 中央处理器(CPU):含有运算器和控制器(算数计算+逻辑计算)等
- 输出单元:显示器,打印机,音响,显卡等
- 存储器:就是内存
关于冯诺依曼,必须强调几点:
- 这里的存储器指的是内存
- 不考虑缓存情况,这里的CPU能且只能对内存进行读写,不能访问外设(输入或输出设备)
- 外设(输入或输出设备)要输入或者输出数据,也只能写入内存或者从内存中读取。
- 所有设备都只能直接和内存打交道。
为什么要有内存?
技术角度:cpu的运算速度>寄存器的速度>L1~L3缓存(三级缓存)>内存>>外设(磁盘)>>光盘磁带
cpu的速度比外设速度快太多了,外设直接与cpu交互cpu速度大幅度下降,导致计算机效率低下
数据角度:外设不和cpu直接交互,而是和内存交互,cpu也是和内存交互(数据交互层面)
内存就是体系结构的一个大的缓存,适配外设和cpu速度不均的问题!
成本角度:寄存器>>内存>>磁盘(外设)
寄存器的造价成本太高,如果使用寄存器代替内存会导致计算机成本价格太高了,但是计算机蔓延全世界的优点得益于它的有效+便宜
对冯诺依曼的理解,不能停留在概念上,要深入到对软件数据流理解上!
问题1:
试着描述从你登录上qq开始和某位朋友聊天开始,数据的流动过程从你打开窗口,开始给他发消息,到他的到消息之后的数据流动过程。
输入设备读取数据流,数据流进入存储器存储,数据流进入运算器封装,封装完后进入存储器存储,数据流进入输出设备进行输出
试着解决问题2:
如果是在qq上发送文件,过程是怎样的?(本质上是磁盘之间的通信)
操作系统(Operator System)
概念
任何计算机系统都包含一个基本的程序集合,称为操作系统(OS)。笼统的理解,操作系统包括:
- 内核(进程管理,内存管理,文件管理,驱动管理)
- 其他程序(例如函数库,shell程序等等)
设计OS的目的
- 与硬件交互,管理所有的软硬件资源
- 为用户程序(应用程序)提供一个良好的执行环境
定位
在整个计算机软硬件架构中,操作系统的定位是:一款纯正的“搞管理”的软件
如何理解 "管理"
管理的本质:对数据做管理
对数据管理--->对某种数据结构的管理
管理的核心理念:先描述,再组织
总结
计算机管理硬件
- 描述起来,用struct结构体
- 组织起来,用链表或其他高效的数据结构
系统调用和库函数概念
操作系统不相信任何人,要防止少数人,又要给多数人提供服务,因此操作系统是通过给用户提供接口的方式来调用操作系统的功能,Linux内核使用C语言写的!这里的接口就是用C语言给我们提供的函数调用!而这个函数调用就是系统调用!
- 在开发角度,操作系统对外会表现为一个整体,但是会暴露自己的部分接口,供上层开发使用,这部分由操作系统提供的接口,叫做系统调用。
- 系统调用在使用上,功能比较基础,对用户的要求相对也比较高,所以,有心的开发者可以对部分系统 调用进行适度封装,从而形成库,有了库,就很有利于更上层用户或者开发者进行二次开发。
如下图,是一张软硬件的计算机体系图:
通过图中可以发现操作系统给用户提供服务是通过系统调用接口提供服务的。
进程
基本概念
- 课本概念:程序的一个执行实例,正在执行的程序等
- 内核观点:担当分配系统资源(CPU时间,内存)的实体。
进程=可执行程序+该进程对应的内核数据结构
描述进程-PCB
- 进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合。
- 课本上称之为PCB(process control block),Linux操作系统下的PCB是: task_struct
task_struct-PCB的一种
- 在Linux中描述进程的结构体叫做task_struct。
- task_struct是Linux内核的一种数据结构,它会被装载到RAM(内存)里并且包含着进程的信息。
task_ struct内容分类
- 标示符: 描述本进程的唯一标示符,用来区别其他进程。
- 状态: 任务状态,退出代码,退出信号等。
- 优先级: 相对于其他进程的优先级。
- 程序计数器: 程序中即将被执行的下一条指令的地址。
- 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
- 上下文数据: 进程执行时处理器的寄存器中的数据。
- I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
- 记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
- 其他信息
组织进程
可以在内核源代码里找到它。所有运行在系统里的进程都以task_struct链表的形式存在内核里。
查看进程
进程的信息可以通过 /proc 系统文件夹查看
- 如:要获取PID为1的进程信息,你需要查看 /proc/1 这个文件夹。
- 大多数进程信息同样可以使用top和ps这些用户级工具来获取
ps axj是查看进程最常用的命令
通过系统调用获取进程标示符
- 进程id(PID)
- 父进程id(PPID)
如下图方式可查看系统中实时进程的pid和ppid
通过如下图方式即可查看想查看进程的pid和ppid
根据pid在/proc查看确实有该文件:
当进程被kill之后就找不到了:
通过pid查看文件在/proc中的文件信息可知重要的两点:
第一个是进程当前的工作路径;
第二个是进程对应的可执行程序的磁盘文件
注:我们常说的当前路径指的就是进程当前的工作路径
(ps:pid和当前路径都属于进程的内部属性,都保存在进程的控制块PCB中)
获取当前程序的pid和ppid方法:
先使用man 2 getpid
知道使用getpid和getppid函数需要的头文件
然后使用如下代码创建起进程:
然后通过下面方式验证确实如此:
(这里有一个很有意思的点,重复杀死重启几次程序,每次都去查看ppid所对应的进程会发现都是bash,因为几乎我们在命令行上所执行的所有的指令都是bash进程的子进程)
通过系统调用创建进程-fork初识
- 运行 man fork 认识fork
- fork有两个返回值
- 父子进程代码共享,数据各自开辟空间,私有一份(采用写时拷贝)
- fork 之后通常要用 if 进行分流
代码创建子进程fork():
使用man 2 fork命令查看fork的使用及需要的头文件:
根据上面的要求写出如下代码:
运行起来发现确实打印了pid和ppid,if else和两个死循环同时进行了!
出现上面场景的原因是:
fork之后,父进程和子进程共享代码,一般都会执行后续的代码
fork之后,父进程和子进程返回值不同,可以通过不同返回值判断,让父子执行不同的代码块
fork之后为什么给父进程返回子进程的pid?
答:父进程必须有表示子进程的方案!
fork之后为什么给子进程返回0
答:告知子进程自己被创建成功!
进程状态(重点)
操作系统状态:
运行态:进程只要在运行队列中叫做运行态,代表我已经准备好了,随时可以调度
终止态:进程还存在,只不过永远不运行了,随时等待被释放(标记成终止态是为了让操作系统有时间再来释放资源)
阻塞态:进程等待某种资源(非CPU),资源没有就绪的时候,进程需要在该资源的等待队列中进行排队,此时进程的代码并没有运行,进程所处的状态就叫做阻塞
挂起态:一个进程的代码和数据因为操作系统的资源不足,而导致操作系统将该进程的代码和数据临时的置换到磁盘上(磁盘上的swap分区)就叫做进程挂起
Linux操作系统状态:
进程状态查看
ps aux / ps axj 命令
为了弄明白正在运行的进程是什么意思,我们需要知道进程的不同状态。一个进程可以有几个状态(在 Linux内核里,进程有时候也叫做任务)。 下面的状态在kernel源代码里定义:
/*
* The task state array is a strange "bitmap" of
* reasons to sleep. Thus "running" is zero, and
* you can test for combinations of others with
* simple bit tests.
*/
static const char * const task_state_array[] = {
"R (running)", /* 0 */[重点]
"S (sleeping)", /* 1 */[重点]
"D (disk sleep)", /* 2 */
"T (stopped)", /* 4 */[重点]
"t (tracing stop)", /* 8 */
"X (dead)", /* 16 */
"Z (zombie)", /* 32 */[重点]
};
- R运行状态(running): 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里。
R状态对应操作系统的运行态。
执行一个死循环的程序并且查看该进程的状态发现这个名为process的进程状态是运行态(一直处于运行队列中,即处于运行状态)
- S睡眠状态(sleeping): 意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠 (interruptible sleep))。
S状态对应操作系统的阻塞态,也可以是挂起态。
用户可以通过指令或者操作系统自身也可以杀掉该进程。
执行一个访问外设(这个访问的外设是显示器)的程序并且查看该进程的状态发现这个名为process的进程状态是睡眠状态(大部分时间在等待外设资源,即处于阻塞状态)
- D磁盘休眠状态(Disk sleep)有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。
D状态也是一种睡眠状态,即对应操作系统的阻塞态,也可以是挂起态。
一般而言Linux中,如果我们等待的是磁盘资源,我们进程阻塞所处的状态就是D状态,凡是D状态的进程操作系统无权杀掉该进程,即只能等待该进程访问的磁盘读写完成
- T停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。
T/t状态对应操作系统的挂起态,但是处于T/t状态不一定是挂起态,也可能是用户主动暂停进程。
在源码中发现有T有两种状态,两者都是暂停。
查看T(stopped)状态首先写一个死循环代码,然后使用kill -l查看kill的指令集,
我们给进程发送19号信号就会发现进程被暂停了
查看发现进程的状态变成了T,这里想要进程继续只需要给进程发送18号信号即可。
t状态(tracing stop 追踪状态)被设计出来是为了面对一种特殊情况 ,进程被调试的时候遇到断点处的状态
我们使用gdb调试刚写好的代码并且查看被调试进程的状态:
我们会发现当被打断点的程序运行起来并且遇到断点停下来后该进程的状态就是t状态
- X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态。
X状态对应操作系统的终止态。
表示资源可以立马被回收
Linxu操作系统非常特殊的一种状态——Z(zombie)-僵尸进程
Z状态对应操作系统的终止态
当一个Linux中的进程退出的时候,一般不会直接进入X状态,而是进入Z状态,因为一般需要将进程的执行结果告知给父进程,因此进程进入Z状态就是为了维护退出信息,可以让父进程或者操作系统读取的
- 僵死状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程(使用wait()系统调用) 没有读取到子进程退出的返回代码时就会产生僵死(尸)进程
- 僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。
- 所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态
执行下面的代码查看并验证僵尸进程:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
pid_t id = fork();//子进程和父进程共享代码,但两个进程的返回值不同
//成功后给子进程返回0,当子进程退出后父进程仍然在运行,验证子进程退出后的状态
if(id == 0)
{
int cnt = 5;
while(cnt--)
{
printf("%d\n", cnt);
sleep(1);
}
printf("变成了僵尸进程\n");
exit(0);
}
//给父进程返回子进程的pid
else
{
while(1)
{
sleep(1);
}
}
return 0;
}
使用下面的监控脚本每隔一秒钟查看父进程和子进程的状态:
while :; do ps ajx | head -1 && ps ajx | grep process | grep -v grep; sleep 1; done
当子进程没有退出时子进程和父进程都是S状态,当子进程退出后可以看到子进程的状态已经变成了Z状态而非X状态,父进程仍然是S状态,验证了当进程结束后并非直接进入X状态,而是先进入Z状态
注:这里无法通过指令杀死子进程来结束Z状态,因为Z状态已经算是一种特殊的死亡状态了,无法再杀死该进程了,必须让父进程回收子进程
僵尸进程危害
如果父进程不回收子进程则Z状态会一直被维护,该进程相关资源(task_struct)不会被释放,会导致内存泄漏,因此必须要求父进程回收子进程!
注:在Linux中,在状态后有+表示该进程是前台进程,可以使用ctrl+c杀死该进程,没有+表示该进程是后台进程,无法使用ctrl+c杀死该进程,只能使用命令行方式杀死,即使用kill指令
孤儿进程
当出现父进程提前退出,那么子进程后退出,进入Z之后,那如何处理该情况?
父进程先退出,当父进程退出后子进程还没退出,子进程此时就被称为”孤儿进程”
对僵尸进程的代码进行修改后得到如下代码:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
pid_t id = fork();//子进程和父进程共享代码,但两个进程的返回值不同
//成功后给子进程返回0
if(id == 0)
{
while(1)
{
sleep(1);
}
}
//给父进程返回子进程的pid,父进程先退出,子进程没有退出
else
{
int cnt = 5;
while(cnt--)
{
printf("%d\n", cnt);
sleep(1);
}
exit(0);
}
return 0;
}
然后继续运行监控脚本,我们会发现,当父进程结束后并且父进程中没有对子进程进行处理而子进程在父进程结束后仍然存在,子进程的ppid变成了1:
在Linux中,如果父进程提前退出,子进程还在运行,子进程会被1号进程(操作系统)领养 ,这种被1号进程领养的进程就叫做孤儿进程
这里发现子进程被1号进程领养后就变成了后台进程,只能使用kill指令杀死该进程或者孤儿进程被1号init进程领养,要有init进程回收。
进程优先级
基本概念
- cpu资源分配的先后顺序,就是指进程的优先权(priority)。
- 优先权高的进程有优先执行权利。配置进程优先权对多任务环境的linux很有用,可以改善系统性能。
- 还可以把进程运行到指定的CPU上,这样一来,把不重要的进程安排到某个CPU,可以大大改善系统整体性能。
查看系统进程
在linux或者unix系统中,用ps –l命令则会类似输出以下几个内容:
我们很容易注意到其中的几个重要信息,有下:
- UID : 代表执行者的身份
- PID : 代表这个进程的代号
- PPID :代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号
- PRI :代表这个进程可被执行的优先级,其值越小越早被执行
- NI :代表这个进程的nice值
PRI and NI
- PRI即进程的优先级,或者通俗点说就是程序被CPU执行的先后顺序,此值越小进程的优先级别越高
- NI是nice值,其表示进程可被执行的优先级的修正数值 PRI值越小越快被执行,那么加入nice值后,将会使得PRI变为:PRI(new)=PRI(old)+nicev(每次设置优先级,old值都会被恢复成初始值80)
- 当nice值为负值的时候,那么该程序将会优先级值将变小,即其优先级会变高,则其越快被执行
- 调整进程优先级,在Linux下,就是调整进程的nice值
- Linux不允许无节制的设置优先级,因此nice其取值范围是-20至19,一共40个级别。(实际上是140个级别,但是我们只能操作40个级别)
PRI vs NI
- 需要强调一点的是,进程的nice值不是进程的优先级,他们不是一个概念,但是进程nice值会影响到进程的优先级变化。
- 可以理解nice值是进程优先级的修正修正数据
查看进程优先级的命令
用top命令更改(注意需要root权限)已经存在进程的nice(Linux有提供nice和renice修改):
- top
- 进入top后按"r"->输入进程PID->输入nice值
可以看到修改成功了,但是一般我们不会去修改进程的优先级。
其他概念(了解向)
- 竞争性: 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高 效完成任务,更合理竞争相关资源,便具有了优先级
- 独立性: 多进程运行,需要独享各种资源,多进程运行期间互不干扰
- 并行: 多个进程在多个CPU下分别,同时进行运行,这称之为并行
- 并发: 多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发
- 进程抢占:当正在运行的低优先级进程,来了优先级更高的进程,调度器直接将低优先级进程从CPU中剥离,放上优先级更高的进程