进程、线程、协程的区别

线程是指进程内的一个执行单位。也是进程内的可调度实体。

进程与线程的区别:

  • 拥有资源
    进程是资源分配的基本单位,但是线程不拥有资源,线程可以访问隶属进程的资源。

  • 调度
    线程是独立调度的基本单位,进程的一个执行流,是 CPU 调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。

  • 系统开销
    由于创建或撤销进程时,系统都要为之分配或回收资源,如内存空间、I/O设备等,所付出的开销远大于创建或撤销线程时的开销。类似地,在进行进程切换时,涉及当前执行线程 CPU 环境的保存及新调度进程 CPU 环境的设置,而线程切换时只需要保存和设置少量寄存器内容,开销很小。

  • 通信方面
    线程间可以通过直接读写同一进程中的数据进行通信,但是进程通信需要借助 IPC(进程间通信)

协程与线程进行比较

协程是一种用户态的轻量级线程,协程的调度完全由用户控制。协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。
(1)一个线程可以多个协程,一个进程也可以独立拥有多个协程。
(2)线程和进程都是同步机制,而协程则是异步。
(3)协程能保留上一次调用时的状态,每次过程重入时,就相当于进入上一次调用的状态。
实例:项目使用到了多个进程和每个进程下有多个线程,还有线程之间通信比较方便,而进程之间通信需要共享内存、消息队列等方式。

进程间通信(IPC)有哪些方式?

消息队列、共享内存、无名管道、命名管道、信号量、socket

  • 父子进程关系管道 pipe:
    管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常指父子进程关系。

  • 命名管道 FIFO:
    有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。

  • 消息队列 MessageQueue:
    消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限制等缺点。

  • 共享存储:
    共享内存就是映射一段能被其他进程所访问的内存,这段内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量,配合使用,来实现进程间的同步和通信。

  • 信号量:
    信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。

  • 套接字:
    也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同机器间的进程通信。

  • 信号:
    信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。

死锁和处理方法

必要条件

  • 互斥:每个资源要么已经分配给了一个进程,要么就是可用的。
  • 占有和等待:已经得到了某个资源的进程可以再请求新的资源。
  • 不可抢占:已经分配给一个进程的资源不能强制性地被抢占,它只能被占有它的进程显式地释放。
  • 循环等待:有两个或者两个以上的进程组成一条环路,该环路中的每个进程都在等待下一个进程所占有的资源。

处理方法

  • 鸵鸟策略:忽略死锁。
  • 死锁检测与死锁恢复:不试图阻止死锁,而是当检测到死锁发生时,采取措施进行恢复,利用抢占恢复,回滚恢复,通过杀死进程恢复。
  • 死锁预防:破坏死锁必要条件。
  • 死锁避免:程序运行期间避免死锁。(银行家算法)

什么是活锁?

活锁恰恰与死锁相反,死锁是大家都拿不到资源都占用着对方的资源,而活锁是拿到资源却又相互释放不执行。当多线程中出现了谦让,都主动将资源释放给别的线程使用,这样这个资源在多线程之间跳动而又得不到执行,这就是活锁。

I/O 管理

1、网络请求处理过程

图片说明

主要分为两个阶段:
1)web服务器进程等待内核缓冲区的数据准备好;
2)数据准备好后,从内核向进程复制数据。

2、IO 模型:

(1)阻塞式 I/O(blocking I/O):

图片说明
1)web服务器进程(用户进程)调用了系统调用后,内核空间就开始了 IO 的第一个阶段:准备数据(对于网络IO来说,很多时候数据在一开始还没有到达。比如,还没有收到一个完整的UDP包。这个时候内核就要等待足够的数据到来)。这个时候 web服务器进程 是处于一个阻塞状态。

2)当内核一直等到数据准备好了,它就会将数据从内核中拷贝到用户内存,然后内核返回结果,用户进程才解除阻塞状态,重新运行起来。

优点:程序简单,在阻塞等待数据期间进程/线程挂起,基本不会占用 CPU 资源。
缺点:每个连接需要独立的进程/线程单独处理,当并发请求量大时为了维护程序,内存、线程切换开销较大,实际生产上很少使用。

(2)非阻塞 I/O(non-blocking I/O):

图片说明
1)web服务器进程(用户进程)调用了系统调用后,如果内核缓冲区中的数据还没有准备后,用户进程把一个套接口设置为非阻塞,当所请求的 I/O 操作无法完成时,不要将进程睡眠。而是返回一个错误,用户进程基于 I/O 操作函数将不断地轮询数据是否已经准备好,如果没有准备好,继续轮询,直到数据准备好为止。

2)一旦内核中的数据准备好了,并且又再次收到了用户进程的询问,那么内核马上就将数据拷贝到了用户内存,然后返回。

优点:不会阻塞在内核的等待数据过程,每次发起的 I/O 请求可以立即返回,不用阻塞等待,实时性较好。
缺点:轮询将会不断地询问内核,这将占用大量的 CPU 时间,系统资源利用率较低,所以一般 Web服务器不使用这种 I/O 模型。

(3)I/O 复用(I/O multiplexing):

图片说明

单个进程具有处理多个 I/O 事件的能力(Java 中 NIO 的能力)。
select,poll,epoll这个函数会不断地轮询所负责的所有socket,当某个socket有数据到达了,就会通知用户进程。

  • select:
    描述符类型是数组实现,fd_setsize 大小默认为1024,有 I/O 事件发生了却并不知道是哪几个流(可能有一个,多个,甚至全部),只能进行 O(n)的无差别轮询。

  • poll:
    描述符类型是链表实现,本质和 select 差不多,由于是链表实现,所以没有最大连接数限制。

  • epoll:
    epoll使用“事件”的就绪通知方式,通过epoll_ctl注册fd,一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd,回调函数内核会将I/O准备好的描述符加入到一个链表中管理,进程调用epoll_wait()便可以得到事件完成的描述符。即epoll只关注活跃的socket。时间复杂度为 O(1)。
    ① LT(电平触发):默认模式,当 epoll_wait() 检测到描述符事件到达时,将此事件通知进程,进程可以不立即处理该事件,下次调用 epoll_wait() 会再次通知进程。
    ② ET(边沿触发):高速模式,通知之后进程必须立即处理事件。减少了同一事件的触发次数,效率更高。

(4)信号驱动式 I/O(signal-driven I/O):

图片说明

在信号驱动式 I/O 模型中,应用程序使用套接口进行信号驱动 I/O,并安装一个信号处理函数,进程继续运行并不阻塞。
当数据准备好时,进程会收到一个 SIGIO 信号,可以在信号处理函数中调用 I/O 操作函数处理数据。

优点:线程并没有在等待数据时被阻塞,可以提高资源的利用率。
缺点:信号 I/O 在大量 IO 操作时可能会因为信号队列溢出导致没法通知。

对于处理 UDP 套接字来说有用,即这种信号通知意味着到达一个数据报,或者返回一个异步错误。但是,对于 TCP 而言,信号驱动的 I/O 方式近乎无用,因为导致这种通知的条件为数众多,每一个来进行判别会消耗很大资源。

(5)异步 I/O 模型(AIO):

图片说明

应用程序告知内核启动某个操作,并让内核在整个操作(包括将数据从内核拷贝到应用程序的缓冲区)完成后通知应用程序。

与信号驱动模型的主要区别在于:信号驱动 I/O 是由内核通知应用程序何时启动一个 I/O 操作,而异步 I/O 模型是由内核通知应用程序 I/O 操作何时完成。

优点:异步 I/O 能够充分利用 DMA 特性,让 I/O 操作与计算重叠。
缺点:要实现真正的异步 I/O,操作系统需要做大量的工作。目前 Windows 下通过 IOCP 实现了真正的异步 I/O。

总结

图片说明

越往后,阻塞越少,理论上效率也是最优。
这五种 I/O 模型中,前四种属于同步 I/O,因为其中真正的 I/O 操作(recvfrom)将阻塞进程/线程,只有异步 I/O 模型才与 POSIX 定义的异步 I/O 相匹配。