基础了解linux
如今操做系统都是采用虚拟存储器,那么对32位操做系统而言,它的寻址空间(虚拟存储空间)为4G(2的32次方)。
操做系统的核心是内核,独立于普通的应用程序,能够访问受保护的内存空间,也有访问底层硬件设备的全部权限。
为了保证用户进程不能直接操做内核(kernel),保证内核的安全,操心系统将虚拟空间划分为两部分,一部分为内核空间,一部分为用户空间。
针对linux操做系统而言,将最高的1G字节(从虚拟地址0xC0000000到0xFFFFFFFF),供内核使用,称为内核空间,而将较低的3G字节(从虚拟地址0x00000000到0xBFFFFFFF),供各个进程使用,称为用户空间。 nginx
为了控制进程的执行,内核必须有能力挂起正在CPU上运行的进程,并恢复之前挂起的某个进程的执行。这种行为被称为进程切换,这种切换是由操做系统来完成的。所以能够说,任何进程都是在操做系统内核的支持下运行的,是与内核紧密相关的。
从一个进程的运行转到另外一个进程上运行,这个过程当中通过下面这些变化:web
保存处理机上下文,包括程序计数器和其余寄存器。windows
更新PCB信息。数组
把进程的PCB移入相应的队列,如就绪、在某事件阻塞等队列。缓存
选择另外一个进程执行,并更新其PCB。安全
更新内存管理的数据结构。服务器
恢复处理机上下文。
注:总而言之就是很耗资源的网络
正在执行的进程,因为期待的某些事件未发生,如请求系统资源失败、等待某种操做的完成、新数据还没有到达或无新工做作等,则由系统自动执行阻塞原语(Block),使本身由运行状态变为阻塞状态。可见,进程的阻塞是进程自身的一种主动行为,也所以只有处于运行态的进程(得到CPU),才可能将其转为阻塞状态。当进程进入阻塞状态,是不占用CPU资源的。数据结构
文件描述符(File descriptor)是计算机科学中的一个术语,是一个用于表述指向文件的引用的抽象化概念。
文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者建立一个新文件时,内核向进程返回一个文件描述符。在程序设计中,一些涉及底层的程序编写每每会围绕着文件描述符展开。可是文件描述符这一律念每每只适用于UNIX、Linux这样的操做系统。
缓存 I/O 又被称做标准 I/O,大多数文件系统的默认 I/O 操做都是缓存 I/O。在 Linux 的缓存 I/O 机制中,操做系统会将 I/O 的数据缓存在文件系统的页缓存( page cache )中,也就是说,数据会先被拷贝到操做系统内核的缓冲区中,而后才会从操做系统内核的缓冲区拷贝到应用程序的地址空间。用户空间无法直接访问内核空间的,内核态到用户态的数据拷贝
思考:为何数据必定要先到内核区,直接到用户内存不是更直接吗?
缓存 I/O 的缺点:
数据在传输过程当中须要在应用程序地址空间和内核进行屡次数据拷贝操做,这些数据拷贝操做所带来的 CPU 以及内存开销是很是大的。
同步(synchronous) IO和异步(asynchronous) IO,阻塞(blocking) IO和非阻塞(non-blocking)IO分别是什么,到底有什么区别?
五种IO Model:
对于一个network IO (这里咱们以read举例),它会涉及到两个系统对象,一个是调用这个IO的process (or thread),另外一个就是系统内核(kernel)。当一个read操做发生时,它会经历两个阶段:
1 等待数据准备 (Waiting for the data to be ready)
2 将数据从内核拷贝到进程中 (Copying the data from the kernel to the process)
记住这两点很重要,由于这些IO Model的区别就是在两个阶段上各有不一样的状况。
在linux中,默认状况下全部的socket都是blocking,一个典型的读操做流程大概是这样:
当用户进程调用了recvfrom这个系统调用,kernel就开始了IO的第一个阶段:准备数据。对于network io来讲,不少时候数据在一开始尚未到达(好比,尚未收到一个完整的UDP包),这个时候kernel就要等待足够的数据到来。而在用户进程这边,整个进程会被阻塞。当kernel一直等到数据准备好了,它就会将数据从kernel中拷贝到用户内存,而后kernel返回结果,用户进程才解除block的状态,从新运行起来。
因此,blocking IO的特色就是在IO执行的两个阶段都被block了。
linux下,能够经过设置socket使其变为non-blocking。当对一个non-blocking socket执行读操做时,流程是这个样子:
从图中能够看出,当用户进程发出read操做时,若是kernel中的数据尚未准备好,那么它并不会block用户进程,而是马上返回一个error。从用户进程角度讲 ,它发起一个read操做后,并不须要等待,而是立刻就获得了一个结果。用户进程判断结果是一个error时,它就知道数据尚未准备好,因而它能够再次发送read操做。一旦kernel中的数据准备好了,而且又再次收到了用户进程的system call,那么它立刻就将数据拷贝到了用户内存,而后返回。
因此,用户进程实际上是须要不断的主动询问kernel数据好了没有。
注意:
在网络IO时候,非阻塞IO也会进行recvform系统调用,检查数据是否准备好,与阻塞IO不同,”非阻塞将大的整片时间的阻塞分红N多的小的阻塞, 因此进程不断地有机会 ‘被’ CPU光顾”。即每次recvform系统调用之间,cpu的权限还在进程手中,这段时间是能够作其余事情的,
也就是说非阻塞的recvform系统调用调用以后,进程并无被阻塞,内核立刻返回给进程,若是数据还没准备好,此时会返回一个error。进程在返回以后,能够干点别的事情,而后再发起recvform系统调用。重复上面的过程,循环往复的进行recvform系统调用。这个过程一般被称之为轮询。轮询检查内核数据,直到数据准备好,再拷贝数据到进程,进行数据处理。须要注意,拷贝数据整个过程,进程仍然是属于阻塞的状态。
IO multiplexing这个词可能有点陌生,可是若是我说select,epoll,大概就都能明白了。有些地方也称这种IO方式为event driven IO。咱们都知道,select/epoll的好处就在于单个process就能够同时处理多个网络链接的IO。它的基本原理就是select/epoll这个function会不断的轮询所负责的全部socket,当某个socket有数据到达了,就通知用户进程。它的流程如图:
当用户进程调用了select,那么整个进程会被block,而同时,kernel会“监视”全部select负责的socket,当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操做,将数据从kernel拷贝到用户进程。
这个图和blocking IO的图其实并无太大的不一样,事实上,还更差一些。由于这里须要使用两个system call (select 和 recvfrom),而blocking IO只调用了一个system call (recvfrom)。可是,用select的优点在于它能够同时处理多个connection。(多说一句。因此,若是处理的链接数不是很高的话,使用select/epoll的web server不必定比使用multi-threading + blocking IO的web server性能更好,可能延迟还更大。select/epoll的优点并非对于单个链接能处理得更快,而是在于能处理更多的链接。)
在IO multiplexing Model中,实际中,对于每个socket,通常都设置成为non-blocking,可是,如上图所示,整个用户的process实际上是一直被block的。只不过process是被select这个函数block,而不是被socket IO给block。
注意1:select函数返回结果中若是有文件可读了,那么进程就能够经过调用accept()或recv()来让kernel将位于内核中准备到的数据copy到用户区。
注意2: select的优点在于能够处理多个链接,不适用于单个链接
linux下的asynchronous IO其实用得不多。先看一下它的流程:
用户进程发起read操做以后,马上就能够开始去作其它的事。而另外一方面,从kernel的角度,当它受到一个asynchronous read以后,首先它会马上返回,因此不会对用户进程产生任何block。而后,kernel会等待数据准备完成,而后将数据拷贝到用户内存,当这一切都完成以后,kernel会给用户进程发送一个signal,告诉它read操做完成了。
blocking和non-blocking的区别在哪,synchronous IO和asynchronous IO的区别在哪?
blocking vs non-blocking。调用blocking IO会一直block住对应的进程直到操做完成,而non-blocking IO在kernel还准备数据的状况下会马上返回。
synchronous IO和asynchronous IO的区别:
二者的区别就在于synchronous IO作”IO operation”的时候会将process阻塞。按照这个定义,以前所述的blocking IO,non-blocking IO,IO multiplexing都属于synchronous IO。有人可能会说,non-blocking IO并无被block啊。这里有个很是“狡猾”的地方,定义中所指的”IO operation”是指真实的IO操做,就是例子中的recvfrom这个system call。non-blocking IO在执行recvfrom这个system call的时候,若是kernel的数据没有准备好,这时候不会block进程。可是,当kernel中数据准备好的时候,recvfrom会将数据从kernel拷贝到用户内存中,这个时候进程是被block了,在这段时间内,进程是被block的。而asynchronous IO则不同,当进程发起IO 操做以后,就直接返回不再理睬了,直到kernel发送一个信号,告诉进程说IO完成。在这整个过程当中,进程彻底没有被block。
select,poll,epoll都属于IO多路复用,而IO多路复用又属于同步的范畴,故,epoll只是一个伪异步而已。
各个IO Model的比较如图所示:
通过上面的介绍,会发现non-blocking IO和asynchronous IO的区别仍是很明显的。在non-blocking IO中,虽然进程大部分时间都不会被block,可是它仍然要求进程去主动的check,而且当数据准备完成之后,也须要进程主动的再次调用recvfrom来将数据拷贝到用户内存。而asynchronous IO则彻底不一样。它就像是用户进程将整个IO操做交给了他人(kernel)完成,而后他人作完后发信号通知。在此期间,用户进程不须要去检查IO操做的状态,也不须要主动的去拷贝数据。
五种IO模型比较:
sellect、poll、epoll三者的区别
poll
它和select在本质上没有多大差异,可是poll没有最大文件描述符数量的限制。
通常也不用它,至关于过渡阶段
epoll
直到Linux2.6才出现了由内核直接支持的实现方法,那就是epoll。被公认为Linux2.6下性能最好的多路I/O就绪通知方法。windows不支持
没有最大文件描述符数量的限制。
好比100个链接,有两个活跃了,epoll会告诉用户这两个两个活跃了,直接取就ok了,而select是循环一遍。
(了解)epoll能够同时支持水平触发和边缘触发(Edge Triggered,只告诉进程哪些文件描述符刚刚变为就绪状态,
它只说一遍,若是咱们没有采起行动,那么它将不会再次告知,这种方式称为边缘触发),理论上边缘触发的性能要更高一些,可是代码实现至关复杂。
另外一个本质的改进在于epoll采用基于事件的就绪通知方式。在select/poll中,进程只有在调用必定的方法后,内核才对全部监视的文件描述符进行扫描,
而epoll事先经过epoll_ctl()来注册一个文件描述符,一旦基于某个文件描述符就绪时,内核会采用相似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait()时便获得通知。
因此市面上上见到的所谓的异步IO,好比nginx、Tornado、等,咱们叫它异步IO,其实是IO多路复用。
select与epoll
# 首先咱们来定义流的概念,一个流能够是文件,socket,pipe等等能够进行I/O操做的内核对象。
# 无论是文件,仍是套接字,仍是管道,咱们均可以把他们看做流。
# 以后咱们来讨论I/O的操做,经过read,咱们能够从流中读入数据;经过write,咱们能够往流写入数据。如今假
# 定一个情形,咱们须要从流中读数据,可是流中尚未数据,(典型的例子为,客户端要从socket读如数据,可是
# 服务器尚未把数据传回来),这时候该怎么办?
# 阻塞。阻塞是个什么概念呢?好比某个时候你在等快递,可是你不知道快递何时过来,并且你没有别的事能够干
# (或者说接下来的事要等快递来了才能作);那么你能够去睡觉了,由于你知道快递把货送来时必定会给你打个电话
# (假定必定能叫醒你)。
# 非阻塞忙轮询。接着上面等快递的例子,若是用忙轮询的方法,那么你须要知道快递员的手机号,而后每分钟给他挂
# 个电话:“你到了没?”
# 很明显通常人不会用第二种作法,不只显很无脑,浪费话费不说,还占用了快递员大量的时间。
# 大部分程序也不会用第二种作法,由于第一种方法经济而简单,经济是指消耗不多的CPU时间,若是线程睡眠了,
# 就掉出了系统的调度队列,暂时不会去瓜分CPU宝贵的时间片了。
#
# 为了了解阻塞是如何进行的,咱们来讨论缓冲区,以及内核缓冲区,最终把I/O事件解释清楚。缓冲区的引入是为
# 了减小频繁I/O操做而引发频繁的系统调用(你知道它很慢的),当你操做一个流时,更多的是以缓冲区为单位进
# 行操做,这是相对于用户空间而言。对于内核来讲,也须要缓冲区。
# 假设有一个管道,进程A为管道的写入方,B为管道的读出方。
# 假设一开始内核缓冲区是空的,B做为读出方,被阻塞着。而后首先A往管道写入,这时候内核缓冲区由空的状态变
# 到非空状态,内核就会产生一个事件告诉B该醒来了,这个事件姑且称之为“缓冲区非空”。
# 可是“缓冲区非空”事件通知B后,B却尚未读出数据;且内核许诺了不能把写入管道中的数据丢掉这个时候,A写
# 入的数据会滞留在内核缓冲区中,若是内核也缓冲区满了,B仍未开始读数据,最终内核缓冲区会被填满,这个时候
# 会产生一个I/O事件,告诉进程A,你该等等(阻塞)了,咱们把这个事件定义为“缓冲区满”。
# 假设后来B终于开始读数据了,因而内核的缓冲区空了出来,这时候内核会告诉A,内核缓冲区有空位了,你能够从
# 长眠中醒来了,继续写数据了,咱们把这个事件叫作“缓冲区非满”
# 也许事件Y1已经通知了A,可是A也没有数据写入了,而B继续读出数据,知道内核缓冲区空了。这个时候内核就告
# 诉B,你须要阻塞了!,咱们把这个时间定为“缓冲区空”。
# 这四个情形涵盖了四个I/O事件,缓冲区满,缓冲区空,缓冲区非空,缓冲区非满(注都是说的内核缓冲区,且这四
# 个术语都是我生造的,仅为解释其原理而造)。这四个I/O事件是进行阻塞同步的根本。(若是不能理解“同步”是
# 什么概念,请学习操做系统的锁,信号量,条件变量等任务同步方面的相关知识)。
#
# 而后咱们来讲说阻塞I/O的缺点。可是阻塞I/O模式下,一个线程只能处理一个流的I/O事件。若是想要同时处理多
# 个流,要么多进程(fork),要么多线程(pthread_create),很不幸这两种方法效率都不高。
# 因而再来考虑非阻塞忙轮询的I/O方式,咱们发现咱们能够同时处理多个流了(把一个流从阻塞模式切换到非阻塞
# 模式再此不予讨论):
# while true {
# for i in stream[]; {
# if i has data
# read until unavailable
# }
# }
# 咱们只要不停的把全部流从头至尾问一遍,又从头开始。这样就能够处理多个流了,但这样的作法显然很差,由于
# 若是全部的流都没有数据,那么只会白白浪费CPU。这里要补充一点,阻塞模式下,内核对于I/O事件的处理是阻
# 塞或者唤醒,而非阻塞模式下则把I/O事件交给其余对象(后文介绍的select以及epoll)处理甚至直接忽略。
#
# 为了不CPU空转,能够引进了一个代理(一开始有一位叫作select的代理,后来又有一位叫作poll的代理,不
# 过二者的本质是同样的)。这个代理比较厉害,能够同时观察许多流的I/O事件,在空闲的时候,会把当前线程阻
# 塞掉,当有一个或多个流有I/O事件时,就从阻塞态中醒来,因而咱们的程序就会轮询一遍全部的流(因而咱们可
# 以把“忙”字去掉了)。代码长这样:
# while true {
# select(streams[])
# for i in streams[] {
# if i has data
# read until unavailable
# }
# }
# 因而,若是没有I/O事件产生,咱们的程序就会阻塞在select处。可是依然有个问题,咱们从select那里仅仅知
# 道了,有I/O事件发生了,但却并不知道是那几个流(可能有一个,多个,甚至所有),咱们只能无差异轮询全部流,
# 找出能读出数据,或者写入数据的流,对他们进行操做。
# 可是使用select,咱们有O(n)的无差异轮询复杂度,同时处理的流越多,每一次无差异轮询时间就越长。再次
# 说了这么多,终于能好好解释epoll了
# epoll能够理解为event poll,不一样于忙轮询和无差异轮询,epoll之会把哪一个流发生了怎样的I/O事件通知我
# 们。此时咱们对这些流的操做都是有意义的。
# 在讨论epoll的实现细节以前,先把epoll的相关操做列出:
# epoll_create 建立一个epoll对象,通常epollfd = epoll_create()
# epoll_ctl (epoll_add/epoll_del的合体),往epoll对象中增长/删除某一个流的某一个事件
# 好比
# epoll_ctl(epollfd, EPOLL_CTL_ADD, socket, EPOLLIN);//有缓冲区内有数据时epoll_wait返回
# epoll_ctl(epollfd, EPOLL_CTL_DEL, socket, EPOLLOUT);//缓冲区可写入时epoll_wait返回
# epoll_wait(epollfd,...)等待直到注册的事件发生
# (注:当对一个非阻塞流的读写发生缓冲区满或缓冲区空,write/read会返回-1,并设置errno=EAGAIN。
# 而epoll只关心缓冲区非满和缓冲区非空事件)。
# 一个epoll模式的代码大概的样子是:
# while true {
# active_stream[] = epoll_wait(epollfd)
# for i in active_stream[] {
# read or write till unavailable
# }
# }
# 举个例子:
# select:
# 班里三十个同窗在考试,谁先作完想交卷都要经过按钮来活动,他按按钮做为老师的我桌子上的灯就会变红.
# 一旦灯变红,我(select)我就能够知道有人交卷了,可是我并不知道谁交的,因此,我必须跟个傻子似的轮询
# 地去问:嘿,是你要交卷吗?而后我就能够以这种效率极低地方式找到要交卷的学生,而后把它的卷子收上来.
#
#
# epoll:
# 此次再有人按按钮,我这不光灯会亮,上面还会显示要交卷学生的名字.这样我就能够直接去对应学生那收卷就
# 好了.固然,同时能够有多人交卷.
IO多路复用的触发方式
# 在linux的IO多路复用中有水平触发,边缘触发两种模式,这两种模式的区别以下:
#
# 水平触发:若是文件描述符已经就绪能够非阻塞的执行IO操做了,此时会触发通知.容许在任意时刻重复检测IO的状态,
# 没有必要每次描述符就绪后尽量多的执行IO.select,poll就属于水平触发.
#
# 边缘触发:若是文件描述符自上次状态改变后有新的IO活动到来,此时会触发通知.在收到一个IO事件通知后要尽量
# 多的执行IO操做,由于若是在一次通知中没有执行完IO那么就须要等到下一次新的IO活动到来才能获取到就绪的描述
# 符.信号驱动式IO就属于边缘触发.
#
# epoll既能够采用水平触发,也能够采用边缘触发.
#
# 你们可能还不能彻底了解这两种模式的区别,咱们能够举例说明:一个管道收到了1kb的数据,epoll会当即返回,此时
# 读了512字节数据,而后再次调用epoll.这时若是是水平触发的,epoll会当即返回,由于有数据准备好了.若是是边
# 缘触发的不会当即返回,由于此时虽然有数据可读可是已经触发了一次通知,在此次通知到如今尚未新的数据到来,
# 直到有新的数据到来epoll才会返回,此时老的数据和新的数据均可以读取到(固然是须要此次你尽量的多读取).
# 下面咱们还从电子的角度来解释一下:
#
# 水平触发:也就是只有高电平(1)或低电平(0)时才触发通知,只要在这两种状态就能获得通知.上面提到的只要
# 有数据可读(描述符就绪)那么水平触发的epoll就当即返回.
#
# 边缘触发:只有电平发生变化(高电平到低电平,或者低电平到高电平)的时候才触发通知.上面提到即便有数据
# 可读,可是没有新的IO活动到来,epoll也不会当即返回.
non-blocking IO:
import time
import socket
sk = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
sk.setsockopt
sk.bind(('127.0.0.1',6667))
sk.listen(5)
sk.setblocking(False)
while True:
try:
print ('waiting client connection .......')
connection,address = sk.accept() # 进程主动轮询
print("+++",address)
client_messge = connection.recv(1024)
print(str(client_messge,'utf8'))
connection.close()
except Exception as e:
print (e)
time.sleep(4)
#############################client
import time
import socket
sk = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
while True:
sk.connect(('127.0.0.1',6667))
print("hello")
sk.sendall(bytes("hello","utf8"))
time.sleep(2)
break
优势:可以在等待任务完成的时间里干其余活了(包括提交其余任务,也就是 “后台” 能够有多个任务在同时执行)。
缺点:任务完成的响应延迟增大了,由于每过一段时间才去轮询一次read操做,而任务可能在两次轮询之间的任意时间完成。这会致使总体数据吞吐量的下降。
IO multiplexing
在非阻塞实例中,轮询的主语是进程,而“后台” 可能有多个任务在同时进行,人们就想到了循环查询多个任务的完成状态,只要有任何一个任务完成,就去处理它。不过,这个监听的重任经过调用select等函数交给了内核去作。IO多路复用有两个特别的系统调用select、poll、epoll函数。select调用是内核级别的,select轮询相对非阻塞的轮询的区别在于—前者能够等待多个socket,能实现同时对多个IO端口进行监听,当其中任何一个socket的数据准好了,就能返回进行可读,而后进程再进行recvfrom系统调用,将数据由内核拷贝到用户进程,固然这个过程是阻塞的。
import socket
import select
sk=socket.socket()
sk.bind(("127.0.0.1",9904))
sk.listen(5)
while True:
r,w,e=select.select([sk,],[],[],5)
for i in r:
# conn,add=i.accept()
#print(conn)
print("hello")
print('>>>>>>')
#*************************client.py
import socket
sk=socket.socket()
sk.connect(("127.0.0.1",9904))
while 1:
inp=input(">>").strip()
sk.send(inp.encode("utf8"))
data=sk.recv(1024)
print(data.decode("utf8"))
为何不调用accept,会反复print?
select属于水平触发
server端并发聊天
#***********************server.py
import socket
import select
sk=socket.socket()
sk.bind(("127.0.0.1",8801))
sk.listen(5)
inputs=[sk,]
while True:
r,w,e=select.select(inputs,[],[],5)
print(len(r))
for obj in r:
if obj==sk:
conn,add=obj.accept()
print(conn)
inputs.append(conn)
else:
data_byte=obj.recv(1024)
print(str(data_byte,'utf8'))
inp=input('回答%s号客户>>>'%inputs.index(obj))
obj.sendall(bytes(inp,'utf8'))
print('>>',r)
#***********************client.py
import socket
sk=socket.socket()
sk.connect(('127.0.0.1',8801))
while True:
inp=input(">>>>")
sk.sendall(bytes(inp,"utf8"))
data=sk.recv(1024)
print(str(data,'utf8'))
注:本文最重要的参考文献是Richard Stevens的“UNIX® Network Programming Volume 1, Third Edition: The Sockets Networking ” http://mp.weixin.qq.com/s__biz=MzA4MjEyNTA5Mw==&mid=2652563599&idx=1&sn=9781747e54d906c0c140228376e671ed&scene=21#wecha t_redirect
https://pymotw.com/2/select/#module-select
http://blog.csdn.net/lingfengtengfei/article/details/12392449
http://www.jb51.net/article/37416.htm
https://pymotw.com/2/select/#module-select