1.用户空间和内核空间1.1为什么要区分用户和内核?发行版可以看作是给linux加了一层壳,任何一个Linux发行版的系统内核都是Linux。我们的应用程序需要通过Linux内核与硬件进行交互。用户应用,如redis、mysql等,其实是没有办法访问我们操作系统的硬件的,所以我们可以通过发行版的shell访问内核,然后通过内核访问计算机硬件计算机硬件包括,如cpu、内存、网卡等。内核(通过地址空间)可以对硬件进行操作,但是内核需要不同设备的驱动程序。有了这些驱动,内核就可以到计算机硬件中进行内存管理、文件系统管理、进程管理等,如果我们要让用户应用程序访问,计算机就必须访问一些暴露给外界的接口,从而简单实现内核的控制,但是内核本身也是一个应用程序,所以它也需要一些内存、cpu等设备资源。用户应用程序本身也在消耗这些资源。如果没有限制,用户可以随意操作我们的资源。造成一些冲突,甚至可能导致我们的系统无法运行,所以我们需要将用户和内核分开1.2进程寻址空间进程的寻址空间分为两部分:内核空间,用户空间什么是寻址什么是什么关于空间?无论是我们的应用程序还是内核空间,都没有办法直接去物理内存,只能分配一些虚拟内存映射到物理内存。当我们的内核和应用程序访问虚拟内存时,会需要一个虚拟地址,它是一个无符号整数。例如,对于32位操作系统,它的带宽是32,它的虚拟地址是2的32次方,也就是说它的寻址范围是0到2的32次方。这个寻址空间对应2的32个字节是4GB。这4GB将有3GB分配给用户空间,1GB分配给内核系统。在Linux中,它们的权限分为0级和3级两个级别,用户空间只能执行受限的Restricted命令(Ring3),不能直接调用系统资源,必须通过内核提供的接口访问内核空间才能执行特权命令(Ring0),调用所有系统资源,所以一般情况下,用户操作运行在用户空间,内核运行的数据在内核空间,在某些情况下,应用程序需要调用一些特权资源在内核空间调用一些操作,所以这时候就需要在用户态和内核态之间进行切换。例如,Linux系统为了提高IO效率,会在用户空间和内核空间都添加缓冲区:写入数据时,必须先将用户缓冲区数据复制到内核缓冲区,然后再写入设备时读取数据,数据必须从device读取到kernelbuffer,然后copy到userbuffer,这个操作:我们的用户在写入和读取数据的时候,会向kernelstate申请,想要读取kernel数据,而内核数据要等待驱动程序从硬件中读取数据,当数据从磁盘加载时,内核会将数据写入内核的缓冲区,然后将数据复制到用户态的缓冲区,然后再回到应用程序,总体来说,速度慢,就是这个原因,为了加快速度,我们希望无论是读取还是等待数据,最好不要等待,或者时间应该是尽可能短。2、网络模型2.1阻塞IO过程1:应用程序要读取数据,但不能直接读取磁盘数据。需要去内核等待内核操作硬件拿到数据。这个过程需要等待是的,等到内核从磁盘加载数据,然后把这个数据写入用户的缓存区。流程2:如果是阻塞IO,那么在整个流程中,从用户发起读请求开始,一直到数据读完,都处于阻塞状态。用户读取数据时,会先发起一个recvform命令,尝试从内核加载数据。如果内核没有数据,用户将等待。这时内核会从硬件中读取数据,内核读取数据后,将数据复制到用户态,返回ok。整个过程都是阻塞等待。这是blockingIO的总结:此时fetch数据(比如网卡数据),数据还没到,内核需要等待数据。此时用户进程也处于阻塞状态。阶段2:数据到达并复制到内核缓冲区,这意味着它已准备就绪。将内核数据复制到用户缓冲区复制进程模型中,用户进程仍然阻塞等待复制完成,用户进程解除阻塞,处理数据。可以看出,在阻塞IO模型中,用户进程在两个阶段都是阻塞的。2.2非阻塞IO顾名思义,非阻塞IO的recvfrom操作会立即返回结果,而不是阻塞用户进程。第一阶段:用户进程尝试读取数据(如网卡数据)。此时数据还没有到达,内核需要等待数据返回给用户进程异常。用户进程得到错误后,再次尝试读取,如此反复,直到数据准备好。阶段2:将内核数据复制到用户缓冲区。在复制过程中,用户进程仍然处于阻塞状态,等待复制完成。用户进程解除阻塞,处理数据可见,在非阻塞IO模型中,用户进程在第一阶段是非阻塞的,在第二阶段是阻塞的。虽然是非阻塞的,但是性能并没有提升。而且busy-waiting机制会导致CPU空闲,CPU占用率会急剧上升。2.3信号驱动信号驱动IO就是与内核建立SIGIO信号关联,并设置回调。当内核有一个FD就绪时,它会发送一个SIGIO信号来通知用户。在此期间,用户应用程序可以执行其他服务而无需阻塞和等待。第一阶段:用户进程调用sigaction,注册的信号处理函数内核成功返回,开始监听FD。用户进程不阻塞等待,可以进行其他服务。当内核数据准备好后,回调用户进程的SIGIO处理函数。Phase2:接收SIGIO回调信号调用recvfrom,读取内核,将数据拷贝到用户空间,用户进程处理数据。当有大量的IO操作时,就会有很多信号。如果SIGIO处理函数不能及时处理,信号队列可能会溢出,内核空间和用户空间频繁的信号交互性能也会降低。2.4异步IO这样不仅用户态尝试读取数据后不会阻塞,内核数据准备好后也不会阻塞。内核处理完所有数据后,内核会把数据处理完成后写入用户态,所以性能极高,没有任何阻塞,全部由内核完成。可以看出,在异步IO模型中,用户进程在两个阶段都处于非阻塞状态。2.5IO多路复用场景介绍为了更好的理解IO,现在假设这样一个场景:一家餐厅A情况:这家餐厅只有一个服务员,顾客排队点餐,像这样:每次顾客拿到一个一顿饭,他要经过两个步骤:想想吃什么。顾客开始点菜,厨师开始做菜。由于餐厅只有一名服务员,一次只能接待一位顾客,还需要等待。现在的客户想着结果,浪费了排队的人很多时间,效率极低。这是阻塞IO。当然,为了缓解这种情况,老板可以多招几个人,但这也会增加成本,而且在客流巨大的情况下,还是不会有很高的效率提升。只有一个服务员,顾客排队点餐。顾客每次排队吃饭,都要经过两个步骤:想吃什么。顾客开始点菜,厨师开始做菜。和A的情况不同的是,这时候服务员会不停地问顾客:“要不要吃西红柿?”米饭上面有鸡蛋吗?鸡蛋牛肉怎么样?肉末茄子呢?……”虽然服务员一直在问,但是在网络中,这并不会提高数据准备的速度,主要还是要等客人来决定。所以,还是赢了'提高餐厅的效率,可能还会有更多的差评。这就是非阻塞IO。情况C:现在这家餐厅只有一个服务员,但不是让顾客排队,而是顾客拿到菜单,通过自己,点完之后通知服务员,像这样:每个排队的客人都想吃说到吃饭,还是两步:看菜单,想好吃什么,通知服务员,我点了。与A和B不同的是,这种情况下,服务员不用再等着顾客想吃什么,通知后,直接去领菜单就好了,相当于一个餐厅同时服务多人同时只有一个服务员,不像A和B一次只能服务一个人。这个时候,餐厅的效率自然会提高不少。映射到我们的网络服务上,是这样的:guest:客户请求订单内容:客户老板发送的实际数据:操作系统人力成本:系统资源菜单:文件状态描述符。操作系统对进程可以同时持有的文件状态描述符的数量有限制。在Linux系统中,使用$ulimit-n查看这个限制值。当然,也可以(并且应该)调整内核参数。Waiter:操作系统内核用于IO操作的线程(内核线程)Chef:应用线程(当然厨房就是应用进程)送餐方式:包括阻塞和非阻塞。方法A:阻塞IO方法B:非阻塞IO方法C:多路复用IO2.6多路复用IO的实现目前进程中多路复用IO的实现主要有四种:select、poll、epoll、kqueue。下表是它们的一些重要特性的比较:IO模型相对性能关键思想操作系统JAVA支持选择更高Reactorwindows/Linux支持,Reactor模式(反应器设计模式)。Linux操作系统2.4内核版本之前,默认使用select;目前Windows下对同步IO的支持是select模型poll,高于ReactorLinuxLinux下的JAVANIO框架,Linux内核2.6版本之前使用poll来支持。也使用Reactor模式,epoll是highReactor/ProactorLinuxLinuxkernels2.6及以后使用epoll进行支持;2.6内核版本之前的Linux内核使用poll来支持;另外必须要注意的是,由于Linux下Windows下没有IOCP技术提供真正的异步IO支持,所以在Linux下使用epoll模拟异步IOkqueueHighProactorLinux目前版本的JAVA不支持多路复用IO技术最适合对于“高并发”场景,所谓高并发是指1毫秒内至少有上千个连接请求准备好。在其他情况下,多路复用IO技术无法发挥其优势。另一方面,使用JAVANIO进行功能实现比传统的Socket套接字实现复杂,所以在实际应用中需要根据自己的业务需求来选择技术。2.6.1selectSelect是Linux最早开发的I/O多路复用技术:在Linux中,一切皆文件,socket也不例外。我们把要处理的数据封装成FD,然后在用户态创建一个fd_setSet(这个set的大小是要监控的FD的最大值+1,但是整体大小是有限制的),这个的长度set是有限的,在这个set中,指明我们要控制哪些数据。其内部流程:在用户态:创建fd_set集合,包括读事件、写事件和需要监控的异常事件的集合。确定要监控的fd_set集合。将要监听的集合作为参数传入select()函数,select将集合复制到内核缓冲区中的内核状态:内核线程获取到集合后遍历集合,没有数据就绪,然后睡觉。当数据到来时,线程被唤醒,然后再次遍历集合,标记就绪的fd,然后将整个集合复制回用户线程遍历用户缓冲区中的集合,找到就绪的fd,并发起读取请求。缺点:setsize固定为1024,也就是说最多只能维护1024个socket。在海量数据的情况下,set不够用,需要在userbuffer和kernelbuffer中反复拷贝,涉及到用户态和内核态的切换,对性能影响很大2.6.2pollpoll方式使得一个对select模式进行简单改进,但性能提升不明显。IO过程:创建一个pollfd数组,将关注的fd信息加入其中,自定义数组大小,调用poll函数,将pollfd数组复制到内核空间,存入链表,无限遍历fdkernel,判断数据是准备好还是超时。将pollfd数组复制到用户空间,返回就绪fd的个数n用户进程判断n是否大于0,如果大于0则遍历pollfd数组,找到就绪fd并与select:大小进行比较select模式下的fd_set固定为1024,而pollfd在内核中使用链表理论上没有上限,但实际做不到,因为监控fd越多,每次遍历的时间就越长服用,性能反而会下降。2.6.3epollepoll模式是对select和poll的改进。三个函数:eventpoll、epoll_ctl、epoll_waiteventpoll函数里面包含两个东西:红黑树:用来记录所有的fd链表:记录准备好的fdepoll_ctl函数,将要监听的fd添加到红黑树中,给每个fd都绑定了一个监控函数,当fd就绪时会触发该监控函数。这个监听函数的操作就是把这个fd加入到链表中。epoll_wait函数,准备等待。开始时,在用户模式缓冲区中创建一个空的事件数组。准备就绪后,我们的回调函数会将fd添加到链表中。函数调用时会检查链表(当然这个过程需要参考配置等待时间,可以等待一定时间,也可以一直等),如果没有fdin链表,然后将fd从红黑树加入到链表中,然后将链表中的fd复制到用户态的空事件中,并返回对应的操作次数。用户态收到响应后,会从事件中获取准备好的数据,调用read方法获取数据。2.6.4小结:select方式存在三个问题:最大可监控的FD数不超过1024,每次select都需要将所有要监控的FD复制到内核空间。每次遍历所有fd判断就绪poll方式问题:poll使用链表解决了select中监听fd上限的问题,但是还是需要遍历所有fd。如果有更多的监控,性能会下降。epoll模式如何解决这些问题呢?在epoll实例中根据红黑树保存需要监控的FD。理论上没有上限,增删改查效率很高。每个FD只需要执行一次epoll_ctl就可以添加到红黑树中,以后不需要为每个epol_wait传递任何参数。不需要反复将fd复制到内核空间。使用ep_poll_callback机制监控FD的状态。不需要遍历所有的FD,所以性能不会随着监控的FD数量的增加而下降。图服务器启动后,服务器会调用epoll_create创建一个epoll实例。epoll实例包含两棵数据红黑树(空):rb_root用于记录需要监听的FD列表(空):list_head,创建就绪的FD后,会调用epoll_ctl函数。该函数会将需要监听的fd添加到rb_root中,并为红黑树中当前存在的节点设置一个回调函数。当这些被监听的fd准备好后,会调用与其关联的回调函数,调用结果为添加后将红黑树的fd添加到list_head中(但此时未完成)fd的完成,会调用epoll_wait函数,这个函数会检查fd是否就绪(因为一旦fd就绪,就会被回调函数添加到list_head中),等待一段时间(可配置)。如果超时时间足够,不返回数据,如果有,进一步判断当前是什么事件,如果是连接建立事件,则调用accept()接受客户端socket,获取建立连接的socket,然后建立一个连接,如果是另一个事件,写出数据。2.8五种网络模型的比较:最后用一张图来说明它们的区别3.Redis通信协议3.1RESP协议Redis是一个CS架构的软件,通信一般分为两步(不包括管道和PubSub):client客户端(client)向服务器(server)发送命令,服务器解析并执行命令,并将响应结果返回给客户端。因此,客户端发送的命令格式和服务器响应结果的格式必须有一个规范。该规范是通信协议。在Redis中,采用了RESP(RedisSerializationProtocol)协议:Redis1.2版本引入了RESP协议。Redis2.0版本成为与Redis服务器通信的标准,称为RESP2。在Redis6.0版本中,协议从RESP2升级到RESP3,增加了支持更多的数据类型,支持6.0-client缓存的新特性,但目前默认还是RESP2协议。在RESP中,第一个字节的字符用于区分不同的数据类型。常用的数据类型有五种:单行字符串:第一个字节为'+',后面是单行字符串,最后以CRLF("\r\n')开头。例如return"OK":"+OK\r\n"错误(Errors):第一个字节为'-',同单行字符串格式,但该字符串为异常信息,例如:"-Errormessage\r\n"值:第一个字节为':',后跟一个数字格式的字符串,以CRLF结尾。例如:":10\r\n"多行字符串:第一个字节为'$',表示二进制安全字符串,最大支持512MB:如果大小为0,表示空字符串:"$0\r\n\r\n"如果大小为-1,表示不存在:"$-1\r\n"array:第一个字节为'*',后面是数组元素的个数,后面是元素,元素数据类型不限:本文由教研组发表传智教育博学谷,如果本文对您有帮助,请关注点赞;如果您有任何建议,也可以留言或私信。您的支持是我坚持创作的动力。转载请注明出处!
