操作系统笔记(一):进程和线程

进程、线程

进程(process)和线程(thread)的区别

进程拥有独立的用户空间;同一个进程的多个线程共享进程的用户空间资源,存在安全问题,在多线程并发的情况下,需要对共享资源进行加锁,以获得正确的结果。
进程优势:进程之间相互独立,可以减少因一个进程异常或者退出带来的风险;进程不需要加锁,可以节省锁带来的消耗。
线程优势:fork进程是消耗很大的。fork要把父进程的内存映像拷贝到子进程,并在子进程中复制所有描述符,如此等等。线程提供一种并发能力,可以在一个进程中的同一时刻运行多个线程。每个线程都有自己的硬件寄存器和堆栈。一个进程中的所有线程共享全部虚拟空间地址、所有文件描述符、信号行为和其他的进程资源。

进程和线程本质上都是CPU的一个工作时间段,进程包括了CPU加载程序上下文、CPU执行、CPU保存程序上下文。线程包含在进程中,一个进程至少有一个线程,也可以有多个线程,进程的不同线程之间共享CPU和程序上下文。
进程是操作系统资源分配(包括CPU、内存、磁盘IO等)的最小单元;线程是CPU调度和分配的基本单元,是最小的执行单元。
PS:不同进程之间的连续虚拟地址空间被MMU映射到不同的离散物理地址,因此不同进程间的用户空间的数据是不共享的。此外,内核空间的代码和数据是不同进程之间共享的。

协程(coroutine),协同程序

线程的切换需要进入内核态,协程的切换完全在用户态进行,效率更高。
协程又称微线程,是一段子程序。在执行过程中,子程序内部可以中断,然后转而执行别的子程序,在适当的时候再返回来接着执行。(在一个子程序中中断,去执行其他子程序,不是函数调用!)
协程是子程序的切换而不是线程的切换,和多线程相比,节省了线程切换的开销,执行效率高。
PS:协程切换完全在用户态进行,它的开销只有切换CPU上下文;线程切换只有最高权限的内核态才能完成,它的开销除了CPU上下文切换,还有用户态和内核态的切换的开销以及线程调度算法完成线程调度的开销。
PS:多线程+协程,即充分用多核,又充分发挥协程的高效率,可以获得极高的性能。

进程调度策略

  1. 先来先服务(FCFS)调度算法:队列实现,先进入作业队列的作业先分配资源、创建进程,然后放入就绪队列,非抢占,平均等待时间通常比较长。
  2. 最短作业优先(SJF)调度算法:对一个或若干个短作业或者短进程优先调度,平均等待时间最小,但无法获知进程下个CPU区间的长度。
  3. 优先级调度算法:优先级越高越先分配,相同优先级按FCFS分配。优先级低的进程无穷等待CPU,会导致无穷阻塞问题,或称为饥饿。可以通过老化技术解决该问题,即逐渐增加在系统中等待时间长的进程的优先级。
  4. 时间片轮转调度算法:专门为分时系统设计,如果进程的CPU区间超过了一个时间片,那么该进程就会被抢占并放回就绪队列。
  5. 多级队列调度算法:将就绪队列分成多个独立的队列,每个队列都有自己的调度算法,队列之间采用固定优先级抢占调度。一个进程根据自身的属性被永久地分配到一个队列中。
  6. 多级反馈队列调度算法:相比于多级队列调度算法,多级反馈队列调度算法允许进程根据实际情况在队列之间移动。若进程使用过多的CPU时间,那么它会被转移到更低的优先级队列;在较低优先级队列等待时间过长的进程会被转移到更高优先级的队列,以防止饥饿发生。

进程间、线程间通讯的方式

参考博客
进程间通讯:管道及命名管道、信号/事件、消息队列、共享内存、信号量、套接字。
管道及命名管道主要用于本地进程间通信,套接字主要用于网络进程间通信,共享内存和信号量主要用于进程间的同步。
线程间通讯:锁机制(互斥量、条件变量、读写锁、自旋锁),信号量机制,信号机制/事件机制。
线程间的通信目的主要是用于线程同步,所以线程没有像进程通信中的用于数据交换的通信机制。

进程间的通信方式——pipe(管道)
消息队列的使用场景是怎样的?

进程、线程的同步方式

进程/线程同步主要是为解决对共享数据(共享区)的竞争访问问题,所以同步是指对共享区的访问同步化(按照既定的先后次序,一个访问需要阻塞等待前一个访问完成后才能开始)。

进程同步:信号量(计数器,PV操作)、自旋锁(调用者循环等待)、管程(管程内定义的操作在同一时刻只被一个进程调用)、会合(会合是适用于不具有公共内存的分布式系统的同步机制)。

Linux 信号量
信号量有两种实现:System V信号量和Posix信号量,System V信号量由semget、semop、semctl这几个函数来实现,Posix信号量由sem_init、sem_destroy、sem_wait(相当于P操作)、sem_post(相当于V操作)这几个函数来实现。(System V和Posix都是应用于操作系统的接口协议)
信号量的P、V操作:信号量只有两种操作,等待和发送信号,分别用P(s)、V(s)表示。P、V操作是不可分割的(原子操作)。调用P操作测试消息是否到达,调用V操作发送消息。

  • P(s): 进程申请资源,如果资源已达到最大进程访问限制(s为0),则需要等待。如果s的值大于0,那么P将s的值减1。如果s为0,那么就挂起这个进程直到s变为非零。一个V操作会唤醒这个线程。
  • V(s):释放资源。将s加1,如果有进程等待s变为非0,那么唤醒该进程,该进程将s减1。

详细解释:将s设置为访问此资源的最大进程数目,每增加一个进程对共享资源的访问,s减1,只要当s大于0,就可以发出信号量,允许申请资源的进程访问该共享资源。但当s减小到0时,说明当前占用资源的进程数已经达到了所允许的最大数目,不允许其他进程访问该共享资源,此时的信号量将无法发出。进程在处理完共享资源后,s加1。

线程同步:临界区(Critical Section)、锁机制【互斥锁(Mutex)和条件变量(Condition_variable)、读写锁(Read-write lock)、自旋锁(Spin lock)】、信号量(Semaphore)、信号/事件(Event)。

概括:

  • 临界区通过对多线程的串行化来访问公共资源或一段代码,速度快;互斥量为协调共同对一个共享资源的访问而设计的;信号量为控制一个具有有限数量用户资源而设计;事件则用来线程有一些事件已经发生,从而启动后继任务的开始。

semaphore和mutex的区别?
信号量与互斥锁的区别:

  • 互斥锁:一个二元变量,0-开锁、1-上锁,开锁操作必须由上锁的线程执行,主要目的是保护共享资源,即保证了共享资源一次只能被一个线程使用(临界资源);
  • 信号量:一个非负整数值,主要目的是调度线程/进程,允许多个线程/进程同一时刻访问一个共享资源,但会限制在同一时刻访问此资源的最大线程/进程数目。如果信号量的值只为0或1,那么它就是一个二元信号量,功能就想当于一个互斥锁。

[多线程] 互斥量和临界区的区别
临界区和互斥锁的区别:

  • 临界区:临界区是指一段代码,这段代码是用来访问临界资源的。临界资源可以是硬件资源,也可以是软件资源。但它们有一个特点就是,一次仅允许一个进程或线程访问。当有多个线程试图同时访问,但已经有一个线程在访问该临界区了,那么其他线程将被挂起。临界区被释放后,其他线程可继续抢占该临界区。
  • 与互斥锁区别:临界区是一种轻量级的同步机制,与互斥和事件这些内核同步对象相比,临界区是用户态下的对象,即只能在同一进程中实现线程互斥。因无需在用户态和核心态之间切换,所以工作效率比较互斥来说要高很多;互斥锁是可以命名的,因此互斥锁不仅可以用于同一应用程序不同线程中共享资源访问的同步,也可以用于不同应用程序的线程之间实现对共享资源访问的同步。(互斥锁可以在整个系统中被任意进程的任意线程访问到,但它严格限定只有获取了互斥量的线程才能释放该互斥锁

用户空间和内核空间、用户态和内核态:

  • 不同进程之间的内核空间的资源是共享的,用户空间的资源是独立的。因此,要实现不同进程之间或者不同进程的线程之间资访问源的同步,必须进入内核态,由内核中的同步对象实现进程同步。而对于同一进程的不同线程之间用户空间和内核空间的资源都是共享的,因此可以只用用户态下的同步对象实现线程的同步。

读写锁:

  • 若当前线程读数据,则允许其他线程读数据,但不允许写;
  • 若当前线程写数据,则不允许其他线程读、写数据。

死锁产生的条件和处理策略

死锁产生的四个必要条件

  1. 互斥:至少有一个资源一次只能被一个进程使用。
  2. 占有并等待:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
  3. 非抢占式:进程已获得的资源在未使用完之前不能被抢占。
  4. 循环等待:若干进程之间形成一种头尾相接的循环等待资源关系。

死锁的处理

  1. 预防死锁:确保死锁产生的四个必要条件中至少有一个不成立。打破“非抢占式”条件,允许进程强行从占有者那夺取某些资源;打破“占有并等待”条件,只允许进程在没有占用资源时才可以申请资源。
  2. 避免死锁:动态地检测资源分配状态,以确保循环等待条件不成立。
  3. 检测、解除死锁:允许死锁发生,但可以检测出死锁的发生并从系统中将已发生的死锁清除,常用的解除死锁的方法为进程终止和资源抢占。

生产者消费者模式

生产者和消费者问题是线程模型的经典问题:生产者和消费者在同一时间段内共用同一个存储空间,生产者往存储空间中添加产品,消费者从存储空间中取走产品,当存储空间为空时,消费者阻塞,当存储空间满时,生产者阻塞
单生产者-单消费者:缓存队列+互斥量+条件变量实现。(队列模拟存储空间;互斥量保证多个读写线程之间互斥;条件变量保证队列为空时消费者线程会被阻塞,等待队列非空,队列为满时生产者线程会被组设,等待队列非满) -> 应用场景:缓存IO(如TCP套接字的缓冲区)
简单的实现:
维护两个位置变量和条件变量:

size_t read_pos; // 消费者读取产品位置.
size_t write_pos; // 生产者写入产品位置.
std::mutex mtx; // 互斥量,保护产品缓冲区
std::condition_variable repo_not_full; // 条件变量, 指示产品缓冲区不为满.
std::condition_variable repo_not_empty; // 条件变量, 指示产品缓冲区不为空.

当(write_pos+1)%repository_size == read_pos时表明队列已满,要阻塞生产。(repository_size为存储空间(缓存)的大小)

...
while(((ir->write_pos + 1) % repository_size) == ir->read_pos) { 
// item buffer is full, just wait here.
    (ir->repo_not_full).wait(lock); // 生产者等待"产品库缓冲区不为满"这一条件发生.
}
... // 写入产品,写入位置后移
(ir->repo_not_empty).notify_all(); // 通知消费者产品库不为空.

当write_pos == read_pos时,表明队列为空,需要阻塞消费者。

...
while(ir->write_pos == ir->read_pos) {
// item buffer is empty, just wait here
    (ir->repo_not_empty).wait(lock); // 消费者等待"产品库缓冲区不为空"这一条件发生.
}
... // 读取产品,读取位置后移
(ir->repo_not_full).notify_all(); // 通知消费者产品库不为满.

多生产者-多消费者:需要额外维护消费者取走产品的计数器和生产者放入产品的计数器,用于判断程序是否要结束。
PS:生产者消费者模式还可以用协程实现。改用协程,生产者生产消息后,直接通过yield 跳转到消费者开始执行,待消费者执行完毕后,切换回生产者继续生产,效率极高。(参考博客
协程

Nginx

Nginx是一个高性能的HTTP和反向代理web服务器。
在这里插入图片描述
面试官常问的Nginx的几个问题

Nginx高性能服务器,为啥高性能?

总结:多进程优势、异步非阻塞I/O(异步主进程由事件驱动+非阻塞socket)、epoll模型(单个进程高效地处理多个连接)。

  • 尽可能限制工作进程的数量,从而减少上下文切换带来的开销。默认和推荐配置是让每个CPU内核对应一个工作进程,从而高效利用硬件资源。
  • 工作进程采用单线程,并以异步非阻塞的事件处理机制来处理多个并发连接,I/O多路复用采用epoll模型

Nginx是如何实现高并发的?

采用了I/O多路复用原理使单个进程能处理多个连接,通过异步非阻塞的事件处理机制,epoll模型,实现轻量级和高并发。
nginx实现高并发的原理
一张图读懂nginx多线程高并发

用进程而不用线程的好处

  • 节省锁带来的开销,每个worker进程都是独立的进程,不共享资源,不需要加锁。同时,在编程及问题查找时,也会方便很多。
  • 独立进程,减少风险。采用独立的进程,可以让互相之间不会影响,一个进程退出后,其它进程还在工作,服务不会中断,master进程则会很快启动新的worker进程。

正向代理和反向代理

  • 正向代理是一个位于客户端和原始服务器(origin server)之间的服务器,为了从原始服务器取得内容,客户端向正向代理发送一个请求并指定目标(原始服务器)。然后,正向代理向原始服务器转交请求并将获得的内容返回给客户端。客户端才能使用正向代理。正向代理总结就一句话:代理端代理的是客户端
  • 反向代理(Reverse Proxy)方式是指以代理服务器来接受internet上的连接请求,然后将请求发给内部网络上的服务器并将从服务器上得到的结果返回给internet上请求连接的客户端,此时代理服务器对外就表现为一个反向代理服务器。反向代理总结就一句话:代理端代理的是服务端

负载均衡

负载均衡即是代理服务器将接收的请求均衡的分发到各服务器中。负载均衡主要解决网络拥塞问题,提高服务器响应速度,服务就近提供,达到更好的访问质量,减少后台服务器大并发压力。

动态资源和静态资源

  • 静态资源:一般客户端发送请求到web服务器,web服务器从内存中取到相应的文件,返回给客户端,客户端解析并渲染显示出来。
  • 动态资源:一般客户端请求的动态资源,先将请求交于web容器,web容器连接数据库,数据库处理数据之后,将内容交给web服务器,web服务器返回给客户端解析渲染处理。

nginx实现tomcat动静分离

以下内容源自:nginx实现tomcat动静分离详解

为什么要实现动静分离

  1. Nginx的处理静态资源能力超强
    主要是nginx处理静态页面的效率远高于tomcat的处理能力,如果tomcat的请求量为1000次,则nginx的请求量为6000次,tomcat每秒的吞吐量为0.6M,nginx的每秒吞吐量为3.6M,可以说,nginx处理静态资源的能力是tomcat处理能力的6倍,优势可见一斑。
  2. 动态资源和静态资源分开,使服务器结构更清晰。

动静分离原理

服务端接收来自客户端的请求中,有一部分是静态资源的请求,例如html、css、js和图片资源等等,有一部分是动态数据的请求。因为tomcat处理静态资源的速度比较慢,所以我们可以考虑把所有静态资源独立开来,交给处理静态资源更快的服务器,例如nginx处理,而把动态请求交给tomcat处理。

如下图所示,我们在机器上同时安装了nginx和tomcat,把所有的静态资源都放置在nginx的webroot目录下面,把动态请求的程序都放在tomcat的webroot目录下面,当客户端访问服务端的时候,如果是静态资源的请求,就直接到nginx的webroot目录下面获取资源,如果是动态资源的请求,nginx利用反向代理的原理,把请求转发给tomcat进行处理,这样就实现了动静分离,提高了服务器处理请求的性能。
在这里插入图片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章