操作系统
操作系统概论
1 基础知识点(了解)
- CMS
Conversational Monitor System 会话监控系统(保证分时共享的单用户,交互式系统) - C/S结构
现代操作系统中普遍的做法采用将操作系统的大部分功能尽量的从核心态中移除,只将最基本的操作组成一个很小的微内核,这就形成了所谓的客户机/服务器的结构的操作系统 - C/S结构图
客户机进程终端服务器进程服务器… …文件服务器内存服务器用户态 微内核核心态
2 内核态
(1)定义
内核态
把需要执行高权限的cpu指令,例如IO操作,内存操作,硬盘操作等,交给Linux操作系统的内核运行,称之为内核态。(对于用户态和内核态,这两种状态是程序运行的两种不同权限模式,用户态可以通过系统调用切换到内核态)
系统调用
从应用程序到访问系统内核资源,称之为系统调用;
(2)内核用户态转换
当一个程序需要操作系统资源的时候,实际上是从【用户态】 -> 【内核态】 -> 【用户态】的过程装换,这个过程中需要一定的开销,主要的执行过程如下:
1)保护用户态线程
2)复制用户态参数
3)参数校验
4)执行内核态代码
5)复制执行结果
6)回复用户态现场
这一次系统调用所产生的开销,主要是通过strace命令,
例子: strace -cp (查看一个进程一段时间内的系统调用,可以看到系统调用的次数和总时间)
(3)陷入内核态
什么时候会陷入内核态
首先内核态的特征是可以直接访问各种硬件设备,当程序需要进行硬件调度的时候会转为内核态,此时的处理是由内核态来进行操作;其次内核态是对硬件资源的分配和使用(其中包括说对内存的分配,io的访问,硬盘的访问,使用cpu的时间,以及各种软硬件的异常处理),所以,总结来说陷入内核态一般都是对于硬件的操作和异常的处理。
避免:个人理解应该是减少io的消耗,硬盘的访问
(4)内核线程
什么是内核线程
Linux的内核可以看做是一个服务进程(管理软硬件资源, 响应应用程序的请求)。内核需要多个执行流并行,防止阻塞。内核线程就是内核的一个分身,每个分身能够处理一个特定的事情。内核线程的调度有内核负责,一个内核线程处于阻塞的状态不影响其他的内核线程,内核线程是调度的基本单位。
3 操作系统的指令
- fork,vfork,clone之前的区别
1)fork函数
创建一个子进程,内核原样复制父进程的整个地址空间,并把复制的内容分配给子进程,pid不同。这种的复制非常的耗时,因为这其中涉及了很多的内存访问,消耗较多的cpu周期,并且完全破坏高速缓存器中的内容。这种操作是没有意义的,因为很多的子进程通过装入一个 新的程序开始他们的执行,这样就相当于完全丢弃了所继承的地址空间。所以引入了写时复制技术(COW copy on write)
COW:内核只为子进程创建虚拟空间,父进程和子进程共享而不是进行复制,但是共享,这就造成了一个问题,就是无法进行修改。这时的父子进程,无论是谁进行一个修改,就会产生一个错误,所以这个时候,内核就将需要修改的内容,复制一个新的出来,并标记为可写,原来的内容还是受保护的状态;当其他进程试图修改时,内核检查当前的内容是否是唯一属性;是的话,内核就将其标志为可写。fork函数的开销,就变成了复制一个内容,以及一个标记位。这种就可以避免在复制地址空间时产生的大量没有必要的开销,而且这些数据大多数的情况下是不进行使用的;
所以总结之后,cow就是在进程空间内的内容发生改变时,才会将父进程的内容复制给子进程;
fork函数,在fork之后,exec之前的两个进程使用的是相同的物理空间(内存区),子进程的代码段,数据段,堆栈都是指向父进程的物理空间,也就是说,父子进程的虚拟空间是不同的,但是物理空间指向的是同一个。当父子进程中发生相应的变化时,再去给子进程分配物理空间。这个过程中,如果不是以因为exec操作的话,父子进程应该是继续共享代码段的物理空间(代码完全相同),但是因为有exec操作,两者执行的代码不同,所以就导致了子进程的代码段,会重新分配单独的物理空间;
一般情况下,fork的子进程会放到队列的前面,已让子进程先执行,以免父进程执行导致写时复制,而后子进程exec系统调用,因无意义的复制造成效率的下降;
2)vfork函数
内核不会创建虚拟地址空间,直接共享父进程的虚拟空间;
vfork()函数和fork()函数类似,都是创建一个子进程,但是vfork()创建子进程之后只有一件事,就是执行_exit()函数或者exec函数族成员,任何其他的函数操作都是不应该的。此外,当调用vfork()之后,父进程会一直阻塞,直到子进程调用_exit()终止,或者调用exec函数族成员,父进程才可能被调度运行,由于子进程在父进程的栈中执行,故父进程会一直堵塞。子进程的任何修改,都会影响父进程的,所以子进程中不能随便调用其他函数;
exit函数是对_exit的封装,在调用_exit前,有很多清理工作,其中包括刷新并关闭当前进程使用的流缓冲;
return函数,子进程return之后,会从当前函数的外部调用后面的代码继续执行,这后面的子进程可能会执行很多语句,无法预料;
3)clone函数
带有参数,能够有选择的进行继承父进程的资源,创造出来的进程可能是父子,也可能是兄弟
三者联系:都是通过调用do_fork函数完成的,do_fork的参数与clone的参数类似,多了一个regs(内核栈保护的用户模式寄存器),其实其他参数也都是这个regs中取得;
fork和vfork的区别:
fork会复制父进程的页表,vfork不会,而是子进程共享父进程
fork使用了写时复制技术,vfork没有使用,而且任何时候都不会进行复制父进程的地址空间
fork的父子进程执行顺序不确定,一般是保证子进程先执行,vfork是必须子进程先执行;
- fork()函数和exec()函数
1)System()函数
启动新进程,建立独立进程,拥有独立的代码空间,内存空间,等待新进程执行完毕,system才返回,原进程阻塞状态;
2)fork()函数
复制进程映像
进程空间中简单分为四个部分:程序段(正文段)、数据段、堆和栈
采用写时复制的fork函数,当执行完的fork函数,一般都是有独立的虚拟空间,但是共享物理空间,当数据发生改变时,就会创建独立的物理空间,但是程序段是属于共享状态,fork之后,父子进程属于独立运行,互不影响;
3)exec()函数
替换进程映像
执行exec函数,原进程将不在执行,新的PID、PPID和nice值与原来的完全一样。exec只是用另一个程序替换了当前进程的正文、数据、堆和栈段。特别注意的是,在原进程中打开的文件描述符,在新进程中仍然是保持打开,除非执行时关闭标志位,被置位。
拓展:
UID:进程的userId
PID:进程id
PPID:父进程id
PRI(priority):优先级,当前值越小,优先级越高
nice:nice值,与优先级有关,不是当前进程的优先级,但是会对优先级有影响;
pri(new) = pri(old) + nice
-
linux常用指令
1)查看 端口情况
nestat
Local Address | State | Foreign Address | PID/program |
---|---|---|---|
本地地址 | 端口状态 | 外部可以访问的ip地址 | 程序id/运行程序 |
2)查看内存情况
free
3)查看进程使用情况
ps -aux
%cpu | %MEM | VSZ | RSS | TTY | STAT | START | TIME | COMMAND |
---|---|---|---|---|---|---|---|---|
占用的 CPU使用率 | 占用的内存使用率 | 占用的虚拟内存大小 | 占用的内存大小 | 终端的次要装置号码 | 该行程的状态 | 行程开始时间 | 执行的时间 | 所执行的指令 |
stat 中的参数意义如下:
D 不可中断 Uninterruptible(usually IO)
R 正在运行,或在队列中的进程
S 处于休眠状态
T 停止或被追踪
Z 僵尸进程
W 进入内存交换(从内核2.6开始无效)
X 死掉的进程
< 高优先级
n 低优先级
s 包含子进程
‘+’ 位于后台的进程组
4 操作系统的处理器管理
linux实时调度策略
(1)SCHED_FIFO
先进先出调度算法,不使用时间片。SCHED_FIFO会比任何的SCHED_NORMAL级的进程都先得到调度。一旦一个SCHED_FIFO级进程处于可执行状态,就会一直执行,直到它自己阻塞或者是显示地释放处理器为止。
由于不依赖时间片,所以能够一直执行下去;
只有较高级的SCHED_FIFO任务或者是SCHED_RR任务才能够抢占SCHED_FIFO的任务。
如果有两个或者多个SCHED_FIFO任务,他们会轮流执行,在他们愿意让出处理器时会再次让出。
只要有SCHED_FIFO级进程在执行,其他级别较低的进程就只能够等待它结束后才能有机会执行;
(2)SCHED_RR
实时轮流调度算法,当SCHED_RR任务耗尽时间片之后,在同一优先级的其他实时进程被轮流调度。时间片只能用来重新调度同一优先级的进程。
对于SCHED_FIFO进程,高优先级总是能够立刻抢占使用,但是低优先级的进程不能够抢占SCHED_RR任务,即使他的时间片耗尽。
这两种调度算法实现都是静态优先级。内核不为实时进程计算动态优先级。这样能够保证给定优先级的实时进程总能抢占优先级比他低的进程。
实时优先级的范围是:0到MAX_RT_PRIO-1.默认MAX_RT_PRIO是100, 所以优先级的范围是0-99.
SCHED_NORMAL级进程的nice值共享了这个取值空间,它的范围是MAX_RT_PRIO 到 MAX_RT_PRIO + 40 也就是 100- 139(nice值是-20 到 19)
5 进程的同步与死锁
-
进程互斥
对共享资源的排他性使用所造成的进程间的间接制约关系称为进程互斥 -
并发(concurrency)
多个执行单元同时、并行被执行,而并发的执行单元对共享资源(硬件资源和软件上的全局变量、静态变量等)的访问容易导致竞态。 -
竞态(linux中的竞态)
- 对称多处理器(SMP)多个cpu,多个cpu使用共同的系统总线,因此访问共同的外设和存储器
- 单cpu内进程与抢占它的进程
- 中断(硬中断,软中断,tasklet,底半部)与进程之间
解决竞态的问题,使用的方法是互斥访问;
-
临界资源(Critical Resource)
在同一时间段只允许一个进程使用的资源
包括:
1)进入区
2)临界区
3)退出区
4)剩余区 -
临界区(Critical Regions)
为保证多个进程对临界资源进行正确的互斥访问,保证这段代码原子的执行,这段代码称为临界区; -
同步机制原则
a. 空闲让进
b. 忙则等待
c. 有限等待
d. 让权等待
两个进程处于临界区,这种情况属于bug,但是如果发生,称之为竞争条件,避免并发和防止竞争条件被称为同步;
- 内核中使用同步的技术
-
中断屏蔽
在单cpu范围内避免竞态的一种方法:在进入临界区之前屏蔽系统的中断,linux内核的进程调度等操作都依赖中断来实现,内核抢占进程之间的并发也就得以避免了。
不能解决多CPU引发的竞态,一般和自旋锁一起使用; -
原子操作
在执行过程中,不会被其他的代码路径所中断的操作。在单处理系统(UniProcessor)中,能够在单条指令中完成的操作都可认为是原子操作,因为中断只能发生于指令之间; -
信号量(Semaphore)
睡眠锁,有任务试图获得一个已经被占用的信号量时,信号量会将其推进一个等待队列,然后让其睡眠,处理器会重获自由,从而执行其他代码,当持有信号量的进程将信号量释放之后,处于等待队列中的任务被唤醒,并获得该信号量;信号量的值确定了同时可以有多少进程可以同时进入临界区,对于信号量的初始值是1,这个信号量就是互斥信号量(MUTEX);
对于大于1的非0值信号量,也可称为计数信号量(counting semaphore)
一般驱动使用的都是互斥信号量 -
RCU(read-copy-update)
- SMP系统中的同步机制
- 自旋锁(spin lock)
自旋锁就是最多只能被一个可执行的线程持有;如果一个执行线程试图获得一个被争用的自旋锁,那么该线程机会一直忙循环-旋转-等待锁重新可用,如果锁未被争用,请求锁的线程能够立刻得到它继续执行;
请求被争用的自旋锁需要等待,比较浪费处理器的时间,所以自旋锁不应该被长时间持有;
还有一种处理锁的争用,让请求线程睡眠,直到锁重新可用在唤醒它,这样处理器就不必循环等待,这也会带来一定的开销,有两次明显的上下文切换,被阻塞的线程要换出和换入-----> 信号量采用这种方式; - 读写自旋锁
读自旋锁:共享自旋锁 同时有多个读执行单元
写自旋锁:排他自旋锁 只能有一个写进程
读写不能同时进行; - 顺序锁(Seq Lock)
对读写锁的一种优化,使用顺序锁,读执行单元不会被写执行单元阻塞;
读执行单元在写执行单元被顺序锁保护的共享资源进行写操作的时候仍然可以进行读操作,不必等待写操作完成;
写执行单元也不用等待所有读执行单元完成操作候才去进行写操作;
注意:
读执行单元和写执行单元之间是互斥的;
读执行单元在读数据期间,写执行单元执行完写操作,读执行单元需要重新进行数据的读取,保证数据的完整;
共享资源中不能含有指针 - 读写信号量
所有的读写信号量都是互斥信号量。
同读写自旋锁一样,读是共享,写是互斥 - 大内核锁BKL(Big Kernel Lock)
全局自旋锁 - Seq锁
用于读写共享数据,实现这样的锁需要依靠一个序列计数器
- 进程通信方式
进程通信方式
linux的进程通信
- 管道
用来连接一个读进程和写进程,实现它们之间通信的共享文件;
开辟固定的缓冲区
单向的字节流
包括:无名管道和命名管道- 无名管道
没有磁盘节点,仅作为一个内存对象存在,用完即销毁
不能显示打开,创建即打开,所以只能父子进程、兄弟进程、其他有亲缘关系的进程并继承了祖先管道文件的两个进程间通信使用;
使用了fork()函数创建一个子进程
没有提供锁机制,必须规定数据流的流动方向 - 命名管道
没有文件名和磁盘节点
任意两个或者多个进程通信使用
提供一个路径名与之关联
以FIFO的文件方式存在文件系统中
- 无名管道
- 信号
进程通信的一种异步通信机制
和中断处理机制比较类似
进程收到某个信号(异步事件)时,插入执行一段制定的程序(信号处理程序),因此进程信号又叫做软中断;发出信号的事件被称为信号源,包括进程、内核、中断和异常;
信号生命周期:信号产生、信号注册、信号注销和信号处理;前两项称之为信号发送,后两项称之为信号接收;
操作系统中的进程都有一个进程控制块(PCB),用于表述和控制单个进程。
PCB包括三个部分:
挂起信号(pending):是一个链表,记录了等待进程处理的信号以及信号的发起者,具有信号寄存器的作用
信号描述符(sig_flag):进程对信号的处理方式和信号处理程序的入口
信号阻塞位(blocked):标记了不处理的信号,具有信号屏蔽寄存器的作用,每个信号都有一个数字标识,在信号描述符中占有相应的表项;
进程接收到信号的四种处理方式:
忽略信号
阻塞信号
默认处理信号
进程处理信号
SysV - 信号量
信号量是一个计数器,可以用来控制说个进程对共享资源的访问。他常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源;
主要是进程之间和同一进程中不同线程之间的同步手段。 - 消息队列
发送方不用等接收方检查收到的消息,接收方没有接收到消息也无需等待;
新消息总是在队尾,取数据不一定在队列头部取,也可中间取数据;
消息队列数据结构存储在内核中;
克服了信号传递信息少,管道只能承载无格式字节流以及缓冲区大小受限等特点; - 共享内存
允许多个进程访问同一块内存;
不同进程把内存映射到自己的一块地址空间,不同的进程映射的空间地址不一定相同;
对地址空间进行写操作时,共享该内存的其他进程会察觉到这个更改,从而实现通信;
没有提供进程间的同步和互斥机制,通常需要和信号量配合使用;
最快捷的
套接字
用于不同机器间的进程通信
- 死锁
- 定义
死锁是指多个进程在运行过程中因争夺资源而造成的一种僵局,当进程处于这种僵局的情况下,没有外力推动,无法向前推进; - 产生条件
- 竞争资源
可剥夺资源(cpu、内存),已经分配给进程的资源,在未使用之前,其他进程可以进程占用的资源
不可剥夺资源:(磁带机、打印机)系统分配给进程后,进程一直占有该资源;除非使用完毕,主动释放,否则不可剥夺的资源
竞争资源一般指的就是不可剥夺资源不足,导致的进程无法完成任务,从而引发的竞争; - 各进程之间的推进顺序不当
进程运行过程中,请求和释放资源的顺序不当,也会造成死锁;
不同进程使用多种不同类型的资源,比如A、B、C三种进程使用资源都需要D、E、F,系统分配后,分别占有一种,进程想要完成任务请求其他两种资源,但是资源的个数分别是1,就造成了死锁;
- 竞争资源
- 死锁的必要条件
互斥条件
不可剥夺条件
请求和保持条件
环路条件 - 死锁的处理方法
预防死锁
避免死锁
检测和解除死锁 - 死锁的预防
摒弃请求和保持条件
资源浪费
进程延迟执行
摒弃不剥夺条件
反复请求,增加系统开销
降低吞吐量
摒弃环路等待条件
资源浪费存在
实现困难 - 死锁的避免
建立资源分配表 - 死锁的解除:
资源剥夺法
选择资源剥夺进程,最小代价
撤销被剥夺资源的工作,最好是完全撤销,终止进程
避免饥饿现象
进程撤销法
全部撤销和最小代价撤销