操作系统总结
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 守护进程、僵尸进程、孤儿进程
1.7 用户级线程和内核级线程
1.8 协程
1.9 父子进程
2.死锁相关
3.虚拟内存相关
3.1 虚拟内存
3.2 分页和分段
3.3 页面置换算法
3.4 缺页中断、颠簸现象
3.5 局部性原理
4. 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方式)。
对比: