5 select 选择的值_总结5

1. Select和epoll

1. 网卡接收数据

网卡收到网线传来的数据;经过硬件电路的传输;最终将数据写入到内存中的某个地址上。这个过程涉及到DMA传输、IO通路选择等硬件有关的知识。

2. 如何知道接收了数据

一般而言,由硬件产生的信号需要cpu立马做出回应(不然数据可能就丢失),所以它的优先级很高。cpu理应中断掉正在执行的程序,去做出响应;当cpu完成对硬件的响应后,再重新执行用户程序。

当网卡把数据写入到内存后,网卡向cpu发出一个中断信号,操作系统便能得知有新数据到来,然后cpu执行中断程序。这个执行过程主要包含两个功能:先将网络数据写入到对应socket的接收缓冲区里面,再唤醒相应的进程,重新将进程放入工作队列中。

3. 进程阻塞

操作系统为了支持多任务,实现了进程调度的功能,会把进程分为“运行”和“等待”等几种状态。运行状态是进程获得cpu使用权,正在执行代码的状态;等待状态是阻塞状态。recv、select和epoll都是阻塞方法。

从普通的recv接收开始分析,先看看下面代码:

//创建socket

int s = socket(AF_INET, SOCK_STREAM, 0);

//绑定

bind(s, ...)

//监听

listen(s, ...)

//接受客户端连接

int c = accept(s, ...)

//接收客户端数据

recv(c, ...);

//将数据打印出来

printf(...)

这是一段最基础的网络编程代码,先新建socket对象,依次调用bind、listen、accept,最后调用recv接收数据。recv是个阻塞方法,当程序运行到recv时,它会一直等待,直到接收到数据才往下执行。

假设工作队列中有A、B和C三个进程

等待队列

当进程A执行到创建socket的语句时,操作系统会创建一个由文件系统管理的socket对象。这个socket对象包含了发送缓冲区、接收缓冲区、等待队列等成员。等待队列指向所有需要等待该socket事件的进程。当程序执行到recv时,操作系统会将进程A从工作队列移动到该socket的等待队列中。由于工作队列只剩下了进程B和C,依据进程调度,cpu会轮流执行这两个进程的程序,不会执行进程A的程序。所以进程A被阻塞,不会往下执行代码,也不会占用cpu资源。

唤醒进程

当socket接收到数据后,操作系统将该socket等待队列上的进程重新放回到工作队列,该进程变成运行状态,继续执行代码。操作系统将进程A唤醒,重新将进程A放入工作队列中。socket的接收缓冲区已经有了数据,recv可以返回接收到的数据。

4. 操作系统如何知道网络数据对应于哪个socket?

因为一个socket对应着一个端口号,而网络数据包中包含了ip和端口的信息,内核可以通过端口号找到对应的socket。当然,为了提高处理速度,操作系统会维护端口号到socket的索引结构,以快速读取。(就是说网卡中断CPU后,CPU的中断函数从网卡存数据的内存,拷贝数据到对应fd的接收缓冲区,具体是哪一个fd,CPU会检查port,放到对应的fd中)

5. 如何同时监视多个socket的数据?

采用多路复用。服务端需要管理多个客户端连接,而recv只能监视单个socket,这种矛盾下,人们开始寻找监视多个socket的方法。

6. select实现思路

select设计思想:假如能够预先传入一个socket列表,如果列表中的socket都没有数据,挂起进程,直到有一个socket收到数据,唤醒进程。

为方便理解,我们先复习select的用法。在如下的代码中,先准备一个数组(下面代码中的fds),让fds存放着所有需要监视的socket。然后调用select,如果fds中的所有socket都没有数据,select会阻塞,直到有一个(也可以是多个)socket接收到数据,select返回,唤醒进程。用户可以遍历fds,通过FD_ISSET判断具体哪个socket收到数据,然后做出处理。

int s = socket(AF_INET, SOCK_STREAM, 0);

bind(s, ...)

listen(s, ...)

int fds[] = 存放需要监听的socket

while(1){

int n = select(..., fds, ...)

for(int i=0; i < fds.count; i++){

if(FD_ISSET(fds[i], ...)){

//fds[i]的数据处理

}

select的实现思路很直接。假如程序同时监视如下图的sock1、sock2和sock3三个socket,那么在调用select之后,操作系统把进程A分别加入这三个socket的等待队列中。当任何一个socket收到数据后,中断程序将唤起进程。假如sock2接收到了数据,中断程序唤起进程A。所谓唤起进程,就是将进程从所有的等待队列中移除,加入到工作队列里面。当进程A被唤醒后,它知道至少有一个socket接收了数据。程序遍历一遍socket列表,就可以得到就绪的socket。

优点:

这种简单方式行之有效,select 在使用前,先将需要监控的描述符对应的bit位置1,然后将其传个select,当有任何一个事件发生时,select将会返回所有的描述符。

缺点:

其一,每次调用select都需要将进程加入到所有监视socket的等待队列,遍历进程A关心的所有socket,每次唤醒都需要从每个队列中移除,其不断在内核态和用户态进行描述符的拷贝,开销很大。

其二,进程被唤醒后,程序并不知道哪些socket收到数据,需要在应用程序自己遍历去检查哪个描述符上有事件发生,效率很低。

7. select函数说明

int select(int maxfdp, fd_set* readfds, fd_set* writefds, fd_set* errorfds, struct timeval* timeout);

先说明两个结构体:

第一:struct fd_set可以理解为一个集合,这个集合中存放的是文件描述符,即文件句柄,这可以是我们所说的普通意义的文件,当然linuix下任何设备、管道、FIFO等都是文件形式,全部包括在内,所以,毫无疑问,一个socket就是一个文件,socket句柄就是一个文件描述符。fd_set集合可以通过一些宏由人为来操作,比如清空集合:FD_ZERO(fd_set*),将一个给定的文件描述符加入集合之中FD_SET(int, fd_set*),将一个给定的文件描述符从集合中删除FD_CLR(int, fd_set*),检查集合中指定的文件描述符是否可以读写FD_ISSET(int, fd_set*)。

第二:struct timeval是一个大家常用的结构,用来代表时间值,有两个成员,一个是秒数,另一个毫秒数。

具体解释select的参数:

l int maxfdp是一个整数值,是指集合中所有文件描述符的范围,即所有文件描述符的最大值加1,不能错!

l fd_set* readfds是指向fd_set结构的指针,这个集合中应该包括文件描述符,我们是要监视这些文件描述符的读变化的,即我们关心是否可以从这些文件中读取数据了,如果这个集合中有一个文件可读,select就会返回一个大于0的值,表示有文件可读,如果没有可读的文件,则根据timeout参数再判断是否超时,若超出timeout的时间,select返回0,若发生错误返回负值。可以传入NULL值,表示不关心任何文件的读变化。

l fd_set* writefds是指向fd_set结构的指针,这个集合中应该包括文件描述符,我们是要监视这些文件描述符的写变化的,即我们关心是否可以向这些文件中写入数据了,如果这个集合中有一个文件可写,select就会返回一个大于0的值,表示有文件可写,如果没有可写的文件,则根据timeout再判断是否超时,若超出timeout的时间,select返回0,若发生错误返回负值。可以传入NULL值,表示不关心任何文件的写变化。

l fe_set* errorfds同上面两个参数的意图,用来监视文件错误异常。

l struct timeval* timeout是select的超时时间,这个参数至关重要,它可以使select处于三种状态。

第一:若将NULL以形参传入,即不传入时间结构,就是将select置于阻塞状态,一定等到监视文件描述符集合中某个文件描述符发生变化为止;

第二:若将时间值设为0秒0毫秒,就变成一个纯粹的非阻塞函数,不管文件描述符是否有变化,都立刻返回继续执行,文件无变化返回0,有变化返回一个正值;

第三:timeout的值大于0,这就是等待的超时时间,即select在timeout时间内阻塞,超时时间之内有事件到来就返回了,否则在超时后不管怎样一定返回,返回值同上述。

l select函数返回值:

负值:select错误

正值:某些文件可读写或出错

0:等待超时,没有可读写或错误的文件

8. epoll实现思路

(1) 功能分离

当程序调用select时,内核会先遍历一遍socket,如果有一个以上的socket接收缓冲区有数据,那么select直接返回,不会阻塞。如果没有socket有数据,进程才会阻塞。select低效的原因之一是将“维护等待队列”和“阻塞进程”两个步骤合二为一。每次调用select都需要这两步操作。epoll将这两个操作分开,先用epoll_ctl维护等待队列,再调用epoll_wait阻塞进程(解耦)。

dd2ff4dcb38e0622340fb39088fa4517.png

为方便理解后续的内容,我们先复习下epoll的用法。如下的代码中,先用epoll_create创建一个epoll对象epfd,再通过epoll_ctl将需要监视的socket添加到epfd中,最后调用epoll_wait等待数据。

int s = socket(AF_INET, SOCK_STREAM, 0);

bind(s, ...)

listen(s, ...)

int epfd = epoll_create(...); //创建一个epoll对象

epoll_ctl(epfd, ...); //将所有需要监听的socket添加到epfd中,可以增加/删除某一个socket

while(1){

int n = epoll_wait(...) //等待直到注册的事件发生

for(接收到数据的socket){

//处理

}

}

(2) 就序链表

select低效的另一个原因在于程序不知道哪些socket收到数据,只能一个个遍历。而epoll避免了无差别遍历,会把哪个socket告诉我们。

epoll_create方法时,内核会创建一个eventpoll对象(也就是程序中epfd所代表的对象),创建了红黑树和就绪链表。红黑树用于存储以后epoll_ctl传来的socket。双向链表,用于存储准备就绪的事件。

epoll_ctl对这个对象进行操作,把需要监控的socket文件描述符添加进去,如果红黑树中存在该描述符,立即返回,不存在则将会以epoll_event结构体的形式组成一颗红黑树。所有添加到epoll中的事件都会与设备(如网卡)驱动程序建立回调关系,也就是说相应事件的发生时会调用这里的回调方法。这个回调方法在内核中叫做ep_poll_callback,它会把这样的事件放到上面的rdllist双向链表中。

epoll_wait调用时,仅仅观察这个rdllist双向链表里有没有数据即可。有数据就返回,没有数据就阻塞,等到timeout时间到后即使链表没数据也返回。

所以,epoll_wait非常高效。epoll_ctl在向epoll对象中添加、修改、删除事件时,从红黑树中查找事件也非常快,它可以轻易地处理百万级别的并发连接。

9. 什么时候select优于epoll

如果在并发量低,socket都比较活跃的情况下,select效率更高,也就是说活跃socket数目与监控的总的socket数目之比越大,select效率越高,因为select反正都会遍历所有的socket,如果比例大,就没有白白遍历。加之于select本身实现比较简单,导致总体现象比epoll好。

2. server端监听端口,没有客户端连接进来,进程处于什么状态?

这个需要看服务端的编程模型,如果如上一个问题的回答描述的这样,则处于阻塞状态,如果使用了epoll,select等这样的io复用情况下,处于运行状态

3. 进程线程的区别

进程:当程序被操作系统装载到内存并分配给它一定资源后,此时可称为进程。

线程:线程是进程内独立执行代码的实体和调度单元,线程依赖于进程而存在。

进程和线程的区别

(1) 划分尺度:线程粒度更小,所以多线程程序并发性更高;

(2) 资源分配&处理器调度:进程是资源分配的基本单位,线程是处理器调度的基本单位。

(3) 地址空间:进程拥有独立的地址空间;线程没有独立的地址空间,同一进程内多个线程共享其资源;

(4)系统开销:创建或撤销进程时,系统都要为进程分配或者回收资源。同样的,进程切换时,涉及到整个进程环境的保存以及新调度进程环境的设置。因此开销比很大。而线程只需要保存和设置少量寄存器内容,开销小很多。

(5)通信:由于同一个进程中的线程有相同的地址空间,所以它们之间的同步和通信的实现,也比较容易。比如IPC通信,线程间可以直接读写进程数据段(全局变量)来来通信,这当然需要进程同步互斥手段来辅助,以保证数据的一致性。

(6) 执行:一个线程只能属于一个进程。进程可以有多个线程。每个线程都有一个程序运行的入口,但线程不能单独执行,线程依赖于进程而存在,一个进程至少有一个主线程。一个线程挂掉将导致整个进程挂掉,而进程间不会相互影响。

4. 线程需要保存那些上下文?

线程在切换过程中,需要保存当前线程id(线程的ID只在它所属的进程环境中唯一)、线程状态、堆栈、寄存器状态等信息。其中寄存器主要包括SP/PC/EAX等寄存器,其主要功能如下:

SP:堆栈指针、指向当前栈顶地址

PC:程序计数器,存储下一条将要执行的指令

EAX:累加寄存器,用于加法乘法的缺省寄存器

5. 进程和线程的通信方式

进程间通信主要包括管道、系统IPC(包括消息队列、信号量、信号、共享内存等)、以及套接字socket

1. 进程间的通信

l 管道

普通管道PIPE

特点:

它是半双工的(即数据只能在一个方向上流动),具有固定的读端和写端。

它只能用于具有亲缘关系的进程之间的通信(也是父子进程或者兄弟进程之间)。

它可以看成是一种特殊的文件,对于它的读写也可以使用普通的read、write 等函数。但是它不是普通的文件,并不属于其他任何文件系统,并且只存在于内存中。

管道的应用:

shell:

管道可用于输入输出重定向,它将一个命令的输出直接定向到另一个命令的输入。比如,当在某个shell程序键入who│wc -l后,相应shell程序将创建who以及wc两个进程和这两个进程间的管道

l FIFO

FIFO,也称为命名管道,它是一种文件类型。

特点:

FIFO可以在无关的进程之间交换数据,与无名管道不同。

FIFO有路径名与之相关联,它以文件形式存在于文件系统中。

l 信号

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

l 系统IPC(Inter-Process Communication进程间通信)

ü 消息队列

消息队列,是消息的链接表,存放在内核中。消息队列中的消息具有特定的格式以及特定的优先级。具有写权限的进程可以按照一定规则向消息队列中的添加新信息,有读权限的进程可以从消息队列中读信息。

ü 信号量

信号量是一个计数器,可以用来控制多个进程对共享资源的访问。信号量用于实现进程间的互斥与同步,而不是用于存储进程间通信数据。

二值信号量:最简单的信号量形式,信号灯的值只能取0或1,类似于互斥锁。

计算信号量:信号量的值可以取任意非负值(当然受内核本身的约束)。

信号量只能进行两种操作等待和发送信号,即P(sv)和V(sv):

P(sv):如果sv的值大于零,就给它减1;如果它的值为零,就挂起该进程的执行

V(sv):如果有其他进程因等待sv而被挂起,就让它恢复运行,如果没有进程因等待sv而挂起,就给它加1.

特点:

信号量用于进程间同步,若要在进程间传递数据需要结合共享内存。

信号量基于操作系统的 PV 操作,程序对信号量的操作都是原子操作。

每次对信号量的 PV 操作不仅限于对信号量值加 1 或减 1,而且可以加减任意正整数。

ü 共享内存

共享内存(Shared Memory),指两个或多个进程共享一个给定的存储区。

特点

共享内存是最快的一种 IPC,因为进程是直接对内存进行存取。

因为多个进程可以同时操作,所以需要进行同步。

信号量+共享内存通常结合在一起使用,信号量用来同步对共享内存的访问。

l 套接字

不同主机间的进程通信

2. 线程间的通信

什么是临界区?

在一段时间内,只允许一个任务(进程或线程)访问的资源叫做临界资源,而访问临界资源的那段代码被称为临界区。线程间的通信目的主要是用于线程同步,所以线程没有像进程通信中的用于数据交换的通信机制。

l 锁机制:包括互斥锁、条件变量、读写锁

互斥锁提供了以排他方式防止数据结构被并发修改的方法。

读写锁允许多个线程同时读共享数据,而对写操作是互斥的。

条件变量可以以原子的方式阻塞进程,直到某个特定条件为真为止。对条件的测试是在互斥锁的保护下进行的。条件变量始终与互斥锁一起使用。

l 信号量机制(Semaphore)

如同进程一样,线程也可以通过信号量来实现通信。它允许多个线程在同一时刻去访问同一个资源,但一般需要限制同一时刻访问此资源的最大线程数目。

l 信号机制(Signal)

类似进程间的信号处理。通过通知的方式来保持多线程同步。

6. 进程状态转换图

进程的五中基本状态:

dd2ff4dcb38e0622340fb39088fa4517.png

多个进程竞争内存资源时,会造成内存资源紧张。并且,如果此时没有就绪进程,处理机会空闲,I/O速度比处理机速度慢得多,可能出现全部进程阻塞等待I/O。针对这种问题,提出了两种解决方法

交换技术:换出一部分进程到外存,腾出内存空间

虚拟存储技术:每个进程只能装入一部分程序和数据

交换技术,进程被交换到外存,从而出现了进程挂起状态:

活动就绪:指进程在内存,处于就绪状态,只要CPU调度就可以运行

静止就绪:进程在外存。是不能被直接调度的状态,只有当主存中没有活跃就绪态进程,或者是挂起态进程具有更高的优先级,系统将把挂起就绪态进程调回主存并转换为活跃就绪。

活动阻塞:指进程在内存,但是被阻塞了。一旦等待的事件产生,便进入活跃就绪状态。

静止阻塞:进程在外存,但是被阻塞了。

活动就绪----静止就绪 内存不够,调到外存

活动阻塞----静止阻塞 内存不够,调到外存

执行 ----静止就绪 时间片用完

7. Fork和vfork的区别

fork特点:fork创建子进程的时候是完全拷贝一份父进程的资源,子进程独立于父进程,子进程对父进程中同名变量进行修改并不会影响其在父进程中的值。父子进程的执行次序不确定。

vfork特点:vfork创建的子进程和父进程共享地址空间,子进程完全运行在父进程的地址空间上,子进程的修改同样对父进程可见,用 vfork创建子进程后,父进程会被阻塞,直到子进程调用exec(进程替换)或exit(进程退出)

注意:vfork时父进程不是只有在子进程exit后才会执行,当子进程执行exec后父进程也会执行。如果在调用这两个函数之前子进程依赖于父进程的进一步动作,则会导致死锁。

8. Reactor模型

在网络服务和分布式对象中,对于网络中的某一个请求处理,我们比较关注的内容大致为:读取请求、 解码请求、处理服务、 编码答复、 发送答复。但是每一步对系统的开销和效率又不尽相同。

A、Classic Service Design

对于传统的服务设计,每一个到来的请求,系统都会分配一个线程去处理,这样看似合乎情理,但是,当系统请求量瞬间暴增时,会直接把系统拖垮。因为在高并发情况下,系统创建的线程数量是有限的。

dd2ff4dcb38e0622340fb39088fa4517.png

改进方法是:

采用基于事件驱动的设计,当有事件触发时,才会调用处理器进行数据处理。

Reactor模式是一种为处理并发服务请求,并将请求提交到一个或者多个服务处理程序的事件设计模式。由一个非阻塞的线程来接收所有的请求,这个线程无线循环去监听是否又客户的请求到来,收到客户端的请求,然后派发这些请求至相关的工作线程进行处理。

dd2ff4dcb38e0622340fb39088fa4517.png

l 初始事件分发器(Initialization Dispatcher):用于管理事件处理器Event Handler,包括定义注册、移除等。它还作为Reactor模式的入口调用同步事件分离器Synchronous Event Demultiplexer的select方法以阻塞等待事件返回,当阻塞等待返回时,根据事件发生的Handle将其分发给对应的事件处理器Event Handler处理,即回调EventHandler中的handle_event()方法。

l 同步(多路)事件分离器(Synchronous Event Demultiplexer):无限循环等待新事件的到来,一旦发现有新的事件到来,就会通知初始事件分发器去调取特定的事件处理器。

l 系统处理程序(Handles):操作系统中的句柄,是对资源在操作系统层面上的一种抽象,它可以是打开的文件、一个连接(Socket)、Timer等。

l 事件处理器(Event Handler): 定义事件处理方法,以供Initialization Dispatcher回调使用。

如何使用Reactor模式?

B、单Reactor单线程模型

Reactor 将I/O事件分派给对应的Handler

Acceptor 处理客户端新连接,并分派请求到处理器链中

Handlers 执行非阻塞读/写任务

Reactor线程,负责多路分离套接字。有新连接到来触发connect 事件之后,交由Acceptor进行处理。所有的IO事件都绑定到selector上,由Reactor分发给handler 处理。Acceptor主要任务就是构建handler,在获取到和client相关的Socket之后 ,绑定到相应的handler上,对应的Socket有读写事件之后,基于racotor 分发,handler就可以处理了。

dd2ff4dcb38e0622340fb39088fa4517.png

C、单Reactor多线程模型

考虑到工作线程的复用,将工作线程设计为线程池。

dd2ff4dcb38e0622340fb39088fa4517.png

D、多Reactor多线程模型

mainReactor 主要是用来处理网络IO 连接建立操作,通常一个线程就可以处理,而subReactor维护自己的selector, 基于mainReactor 注册的socket,多路分离IO读写事件,对业务处理的功能分派给工作线程池来完成。它的个数上一般是和CPU个数等同,每个subReactor一个线程来处理。

此种模型中,每个模块的工作更加专一,耦合度更低,性能和稳定性也大量的提升,支持的可并发客户端数量可达到上百万级别。

dd2ff4dcb38e0622340fb39088fa4517.png

9. 并发并行区别

并发:交替做不同事情。并发表示同时发生了多件事情,通过时间片切换,实现“同时做多件事情”这个效果,但是没有两个任务在同一时刻执行。
并行:同时做不同事情。多核CPU,多个应用程序可以是并行的,互相不影响。

10. 线程池

1. 什么是线程池

线程池就是首先创建一些线程,它们的集合称为线程池。使用线程池可以很好地提高性能,线程池在系统启动时即创建大量空闲的线程,程序将一个任务传给线程池,线程池就会启动一条线程来执行这个任务,执行结束以后,该线程并不会死亡,而是再次返回线程池中成为空闲状态,等待执行下一个任务。可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量。

如果线程本身的开销相对于线程任务执行开销而言是可以忽略不计的,那么此时线程池所带来的好处是不明显的,比如对于FTP服务器以及Telnet服务器,通常传送文件的时间较长,开销较大,那么此时采用线程池未必是理想的方法,可以选择“即时创建,即时销毁”的策略。

2. 线程池的工作机制

在线程池的编程模式下,任务是提交给整个线程池,而不是直接提交给某个线程,线程池在拿到任务后,就在内部寻找是否有空闲的线程,如果有,则将任务交给某个空闲的线程。

一个线程同时只能执行一个任务,但可以同时向一个线程池提交多个任务。

3. 使用线程池的原因

多线程运行时间,系统不断的启动和关闭新线程,成本非常高,会过渡消耗系统资源,以及过渡切换线程的危险,从而可能导致系统资源的崩溃。这时,线程池就是最好的选择了。

4. 线程池的组成部分

l 线程池管理器:创建一定数量的线程,启动线程,调配任务,管理着线程池。

start()创建一定数量的线程池,进行线程循环.

stop()停止所有线程循环,回收所有资源.

addTask()添加任务

l 工作线程:线程池中线程。可以用条件变量实现等待与通知机制。

l 任务接口:每个任务必须实现的接口,以供工作线程调度任务的执行。

l 任务队列:用于存放没有处理的任务。提供一种缓冲机制。

5. 线程池工作的四种情况

l 主程序当前没有任务要执行,线程池中的任务队列为空闲状态。

l 主程序添加小于等于线程池中线程数量的任务。

l 主程序添加任务数量大于当前线程池中线程数量的任务,存入任务缓冲队列,工作线程空闲后主动从任务队列取任务执行。

l 主程序添加任务数量大于当前线程池中线程数量的任务,且任务缓冲队列已满,于是进入等待状态、等待任务缓冲队列中的任务腾空通知。

6. 线程池的实现

l 设置一个生产者消费者队列,作为临界资源

l 初始化n个线程,并让其运行起来,加锁去队列取任务运行

l 当任务队列为空的时候,所有线程阻塞

l 当生产者队列来了一个任务后,先对队列加锁,把任务挂在到队列上,然后使用条件变量去通知阻塞中的一个线程

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值