操作系统总结
1.进程和线程
1.1 区别
- 一个线程只能属于一个进程,线程依赖于进程存在;
- 进程拥有独立的内存空间,线程共享进程的内存空间;
- 进程是系统分配资源和调度的最小单位,线程是CPU调度的最小单位;
- 系统开销。进程的创建、切换的开销远大于线程;
- 通信方式。线程因为共享进程的内存空间,所以通信的时候线程更容易。
1.2 进程的通信
-
管道。管道分为无名管道(PIPE)和有名管道(FIFO)。
1). 无名管道(PIPE)
a.半双工通信,具有固定的写端和读端,数据只能从写端流入,从读端流出;
b.PIPE存在于内存中,因此此方法只用于具有血缘关系之间的双方通信(父子间,兄弟间)
c.PIPE可看作是一种特殊的文件,读写可以使用普通的read()和write()函数。
2). 有名管道(FIFO)
a. FIFO可以实现任意两个进程间通信。
b. FIFO存在于文件系统中,有具体的路径。 -
消息队列。
消息队列:实现形式为一个消息的链接表,存在内核中,一个消息队列由一个标识符标记,具有写权限的进程可以互斥的向消息队列中写消息(写入尾端,通过msgsnd()函数),具有读权限的进程从消息队列中读消息(读队头,通过msgrcv函数)。
int msgsnd ( int msqid, const void *prt, size_t nbytes, int flags);
ssize_t msgrcv ( int msqid, void *ptr, size_t nbytes, long type , int flag); -
共享内存(使用时常出现总线错误【原因:共享文件存储空间大小引起的】。效率最高)。
共享内存允许两个不相关的进程访问同一个逻辑内存,共享内存是两个正在运行的进程之间共享和传递数据的一种非常有效的方式。原理:在Linux中,每个进程都有属于自己的进程控制块(PCB)和地址空间(Addr Space),并且都有一个与之对应的页表,负责将进程的虚拟地址与物理地址进行映射,通过内存管理单元(MMU)进行管理。两个不同的虚拟地址通过页表映射到物理空间的同一区域,它们所指向的这块区域即共享内存。
共享内存虽可多个进程对同一块内存进程操作,但是此方式需要依靠某种同步操作,例如信号量、互斥锁+条件变量等。特点:
- 共享内存是最快的一种通信方式,因为进程直接对内存操作;
- 共享内存本身没有同步操作,需要结合同步方式结合使用。
-
信号量。
信号量的本质是数据操作锁,它本身不具有数据交换的功能,而是通过控制其他的通信资源(文件,外部设备)来实现进程间通信,它本身只是一种外部资源的标识。信号量在此过程中负责数据操作的互斥、同步等功能。信号量就是具有原子性的计数器,就相当于一把锁,在每个进程要访问临界资源时,必须要向信号量拿个锁”,它才能进去临界资源这个“房间”,并锁上门,不让其他进程进来,此时信号量执行P()操作,锁的数目减少了一个,所以计数器减1,;当它访问完成时,它出来,将锁还给信号量,执行V()操作,计数器加1;然后是下面的进程继续。这也体现了各个进程访问临时资源是互斥的。
-
信号。异步通信。通过信号通知接收进程某个事件发生。(开销最小)
-
socket套接字。socket可用于不同主机之间的进程通信,两个进程之间通信的前提是于唯一的表示。网络间通信唯一标识可用(IP地址+协议+端口号)表示,IP地址可唯一标识主机,协议+端口号可唯一标识进程。
1.3 线程的同步
线程的同步可通过:信号量、条件变量+互斥量、读写锁实现。
-
信号量。
信号量:一种特殊的整形变量,可用于线程间同步,只支持PV两种操作。
P(sv):如果信号量sv大于0,则将sv减一;如果sv为0,则将线程挂起。
V(sv):如果有其他线程因等待sv而挂起,则将其唤醒,然后将sv+1;否则直接将sv+1。主要接口:
/*初始化一个信号量; sem:传出参数,创建的信号量; pshared:为0表示用于线程间,为1表示进程间 value:信号量的初始值 */ int sem_init(sem_t *sem,int pshared,unsigned int value); /*销毁一个信号量 */ int sem_destroy(sem_t *sem); /*给信号量加锁,--操作(P) */ int sem_wait(sem_t *sem); /*给信号量解锁,++操作(V) int sem_post(sem_t *sem); */
-
互斥量(互斥锁)。
主要用于线程间互斥,不能保证同步。为了能够完成同步功能,通常于条件变量(条件锁)一起实现同步。当线程进入临界区时,需要先获取到互斥锁并加锁,当离开临界区时,对互斥锁解锁,以唤醒阻塞在此临界区的其他线程。
主要接口/*初始化一个互斥量*/ int pthread_mutex_init(pthread_mutex_t* restrict mutex,const pthread_mutex_attr_t* restrict attr); /*销毁一个互斥量*/ int pthread_mutex_destory(pthread_mutex_t* mutex); /*加锁*/ int pthread_mutex_lock(pthread_mutex_t* mutex); /*解锁*/ int pthread_mutex_unlock(pthread_mutex_t* mutex);
-
条件变量(条件锁)。
可用于线程之间同步分享数据,当摸个共享变量达到一定值时,唤醒等待这个共享变量的一个或者多个线程。
相关接口/*初始化一个条件锁 ;cond:传出参数*/ int pthread_cond_init(pthread_cond_t* restrict cond,const pthread_condattr_t* restrict attr) /*销毁一个条件锁*/ int pthread_cond_destory(pthread_cond_t* cond); /*加锁 1. 阻塞等待条件变量(cond)满足,释放已掌握的互斥量(mutex),相当于pthread_mutex_unlock(&mutex); 2. 当被唤醒时,解除阻塞并重新获取mutex互斥锁,相当于pthread_mutex_lock(&mutex); */ int pthread_cond_wait(pthread_cond_t* restrict cond,pthread_mutex_t* restrict mutex); /*唤醒一个阻塞在条件变量cond上的线程*/ int pthread_cond_signal(pthtread_cond_t* cond); /*唤醒阻塞在条件变量cond上的所有线程*/ int pthread_cond_broadcast(pthread_cond_t* cond);
-
读写锁。
与互斥量类似,但是读写锁有更高的并行性,其特性为:读共享、写独占,写的优先级高(适用于频繁读的情况)。- 写锁状态下,会阻塞所有对该锁加锁的线程。
- 读锁状态下,读锁可以加锁成功,写锁阻塞。
- 无锁状态下,同时加锁,写锁先加。
相关接口
/*初始化一把读写锁*/ int pthread_rwlock_init(pthread_rwlock_t* restrict rwlock,const pthread_rwlockattr* restrict attr); /*以读方式请求读写锁*/ int pthread_rwlock_rdlock(pthread_rwlock_t* rwlock); /*以写方式请求读写锁*/ int pthread_rwlock_wrlock(pthread_rwlock_t* rwlock); /*解锁(解读锁和写锁一个函数)*/ int pthread_rwlock_unlock(pthread_rwlock_t* rwlock);
1.4 为什么要有线程?
进程可以实现多个程序并发执行,以提高资源的利用率和系统的吞吐量,但是进程在同一时间只能干一件事,进程执行过程中如果阻塞,整个进程则会被挂起,即使进程中有些工作不依赖于等待的资源,因此操作系统引入细粒度更低的线程作为并发的基本单位,从而减少程序的并发开销,提高并发性。
1.5 进程的状态
1.创建状态。先由进程申请一个空白的进程控制块(PCB),并向PCB中填写用于控制和管理进程的信息;然后为该进程分配运行时所必须的资源;最后,把该进程转入就绪状态并插入到就绪队列中。
2.就绪状态:进程已经准备好运行的状态,只要再获得CPU,便可立即执行。
3.运行状态:进程已经获取CPU,其进程处于正在执行的状态。
4.阻塞状态:正在执行的进程由于发生某事件(如I/O请求、申请缓冲区失败等)暂时无法继续执行的状态,即进程执行受到阻塞。
5.终止状态:首先,是等待操作系统进行善后处理,最后将其PCB清零,并将PCB空间返还给系统。
1.6 守护进程、僵尸进程、孤儿进程
守护进程:运行在后台的一种特殊进程,独立于控制终端并周期性的执行某些任务。
创建守护进程的步骤: 创建子进程、父进程退出→子进程创建新会话→改变工作目录为根目录→重设文件权限掩码→关闭文件描述符→设置执行任务。
僵尸进程:子进程退出后,父进程未进行回收,子进程的文件描述符仍存在系统中。
孤儿进程:父进程先于子进程退出,子进程成为孤儿进程。孤儿进程会被init进程收养,最后也有init进程回收。
1.7 用户级线程和内核级线程
用户级线程:此类线程的所有工作都由应用程序完成,内核意识不到线程的存在。应用程序通常先在一个线程中运行,即主线程,在运行时可调用线程生成函数创建新的线程。
特点: 高效、不需要进入内核空间,但并发效率不高。
内核级线程:此类线程的所有工作由内核完成,应用程序只能通过调用内核级线程的接口来使用此进程。
特点: 可以将不同线程更好的分配到不同的CPU,实现真正的并行。
1.8 协程
一种更轻量级的存在,协程不被内核所管理,完全由应用程序所管理。子函数(函数)大部分都是层次调用,例:A调B,B调C,C执行完后返回B,最后返回A执行完毕。子程序的调用总是一个入口,依次返回,调用顺序是明确的,而协程的调用是内部可中端的,然后转去执行别的子程序,在适当的时候在返回执行。
一个线程内的多个协程是串行执行的,不能利用多核,所以,显然,协程不适合计算密集型的场景,协程适合I/O 阻塞型。
1.9 父子进程
父进程通过fork()函数可创建子进程。fork()创建子进程时,子进程会复制父进程的所有资源,此时子进程的代码段、数据段、堆栈都是指向父进程的物理空间,但是仅仅是子进程的虚拟空间指向了父进程的物理空间,并不是完全复制。
fork()函数返回给父子进程各一个返回值,>0说明是父进程,=0说明是子进程,<0说明创建失败。
父子进程间遵循“读时共享、写时复制”的原则。
2.死锁相关
死锁定义:两个或多个进程在执行过程中出现互相等待的现象。
死锁的必要条件:
- 互斥条件:进程之间存在互斥访问的资源。
- 请求和保持条件:进程在获得一定资源后,再对其他资源进程申请,申请不到时,进程不会释放原本拥有的资源。
- 不可剥夺条件:进程获得的资源在未完成前不能被剥夺。
- 环路等待条件:各进程形成一种互相等待资源的环形链。
死锁的解决:
- 资源一次性分配;
- 资源可剥夺;
- 资源有序分配。
3.虚拟内存相关
3.1 虚拟内存
虚拟内存为了让物理内存扩充为更大的逻辑内存,从而让程序获得更多的可用内存,防止不同进程在同一时刻使用同一内存。
为了能够更好的管理内存,os将内存抽象为地址空间,每个程序拥有自己的地址空间,此地址空间被分为多个块(每块为一页),这些页会映射到物理内存。虚拟内存允许程序不需要地址空间中的每一页都映射到物理内存,即一个程序不需要全部调入内存就能运行,这使得有限的内存运行大程序成为可能(内存中没有时,页面置换)。
物理内存和虚拟内存的例子:
对于一台内存为256M的32位主机来说,它的虚拟地址空间范围为0~0xFFFFFFFF(4G),而物理地址空间范围为 0 ~ 0x0FFFFFFF(256M)。
虚拟内存的好处
- 可更高效的使用物理内存。通过虚拟内存置换算法在访问后,可以将当前没有访问的物理页先释放掉,释放后的物理页又可以去对应新的虚拟地址。
- 方便内存管理。虚拟内存的为每个进程提供了一致的地址空间,简化内存管理。
- 内存保护。在使用内存时候,暴露给使用者的只是虚拟内存,不会知道具体的物理内存在哪,这提高了系统的封装性。
3.2 分页和分段
段式存储管理是用户视角的内存分配管理方案,在段式存储管理中,将程序的地址空间划分为若干段,如代码段、数据段…。
页式存储管理将程序的物理地址和逻辑地址划分为相同大小的若干页,在程序加载时,可以将任意一页放入内存中任意一页。区别:
- 目的不同。分页是系统管理需要的,而不是用户,页是信息的物理单位;分段是为了满足用户的需要,是信息的逻辑单位。
- 大小不同。页的大小固定且由OS决定,段带下不固定且由程序决定。
- 地址空间不同。页式向用户提供一维空间,短时提供二维空间。
- 碎片不同 。页式存在内碎片,不存在外碎片;段式相反。
3.3 页面置换算法
FIFO:先进先出算法。
LRU:最近最少使用算法。
LFU:最少使用次数算法。
OPT:最优置换算法:根据未来的实际情况将不用的页替换出去,这种算法在使用中没法使用,常作为一种评判指标用。
区分进程调度策略
FCFS:先来先服务算法;
SJF:最短作业优先调度;
优先级调度算法;
时间片轮询调度;
3.4 缺页中断、颠簸现象
缺页中断:在请求分页系统中,可以通过查询页表中的状态为来判断页面是否在内存中,每当要访问的资源页面不在内存中,将发生一次缺页中断.
补 :页表:OS为每一个进程维护了一个从虚拟地址到物理地址的映射关系的数据结构,此数据结构即为页表,页表存储在物理内存中.这个映射的过程由内存管理单元(MMU)完成.
颠簸现象:频繁的调页现象,进程发生缺页中断,被调入内存,然后被调入内存的页又立马被调出内存,重复调入调出的现象就是颠簸.
3.5 局部性原理
局部性原理分为时间局部性和空间局部性.
时间局部性:最近被访问的页在不久后又会被访问;
空间局部性:内存中刚访问页的周围页不久也会被访问;
4. IO模型
对于一次IO访问,数据会先copy到OS内核的缓冲区中,然后才会从OS内核的缓冲区copy到应用程序的地址空间。所以,每一个IO操作,都会经历两个阶段:
- 等待数据准备就绪;
- 将数据从内核copy到进程应用空间。
linux有5种IO模型,分别为:阻塞式IO模型、非阻塞式IO模型、多路复用IO模型、信号驱动IO模型和异步IO模型。
1.阻塞式IO模型。
最传统的一种IO模型,即在读写过程种会发生阻塞现象。在用户线程发出IO请求后,内核会去查看数据是否就绪,如果没有就绪就会等待数据就绪,而用户线程就会处于阻塞状态,用户线程交出CPU。当数据就绪后,内核会将数据copy到用户线程,并返回结果给用户线程,用户线程才会解除阻塞状态。
2. 非阻塞式IO模型
当用户线程发出一个IO操作后(例read()函数),并不需要等待,而是立马返回一个结果。如果返回结果是一个error时,说明数据未准备好,于是再次发送read请求,一旦内核数据准备好,并且收到了用户线程请求,则将数据copy到用户线程。
-
多路复用IO模型
linux种常用的多路复用方法,select、poll、epoll,也称为事件驱动IO。其基本思想为不断轮询所有的socket,当某个socket有数据到达,则通知用户线程。
-
信号驱动IO模型。
在此模型中,当用户线程发起一个IO请求操作,会给对应的socket注册一个信号函数,然后用户线程会继续执行其他的(不阻塞),当内核数据准备好会发送一个信号给用户,用户收到信号后在信号函数中调用IO操作来进行实际的IO请求操作,一般用于UDP通信。
-
异步IO模型。
用户数据发起IO调用后,可立刻转去做其他事情,另一方面,内核会收到read请求后立刻返回,不对用户进程产生任何阻塞。然后,内核会把数据准备好,再copy到用户内存,一切完成后,内核通知用户线程,IO操作完成了。
此模型中,两个阶段都不会阻塞用户线程且由内核完成。用户线程不需自己调用IO函数来进行读写操作(区别与信号IO方式)。
对比:
5.其他
5.1 中断与轮询
中断:CPU执行期间,系统内发生了急需处理的事件,使CPU暂停当前正在执行的程序而转去处理此紧急事件,处理完后再返回继续处理此程序.
轮询:定时对各种设备轮流询问是否有处理要求;
5.2 软链接和硬链接
硬链接:在目录下创建一个新的条目,记录文件名和inode编号,计数器加一;当删除一个条目后,其他的条目还存在,只是计数器减一,当条目数(计数器)为0后,文件才彻底删除.
硬链接不能跨文件系统,且不能为目录建硬链接;
软链接(符号链接):保存源文件的所在路径,不产生一个新的条目,当源文件删除后,软链接即失效;
软链接可为目录建软链接;
5.3 系统调用、内核态与用户态
系统调用指运行在用户态的程序向OS内核请求服务的过程.系统嗲用提供了用户程序与OS内核之间的接口.
用户态和内核态式OS的两种运行级别,两者最大的区别在特权级不同.用户态拥有较低的特权级,内核态拥有较高的特权级.运行在用户态程序不能直接调用内核态的程序和接口.两者之间的切换可通过系统调用、异常和中断实现。
5.4 操作系统定义
控制、管理计算机软硬件资源并合理组织和调度计算器工作和资源分配的一段软件程序。
5.5 并行与并发
并发:同一时刻只能执行一个进程(线程),但是有多个轮流执行,宏观上同时执行.
并行:同一时刻可有多个进程(线程)同时运行.