4.软件架构设计:大型网站技术架构与业务架构融合之道 --- 操作系统

第4章 操作系统 
4.1 缓冲I/O和直接I/O 
	缓冲IO:缓冲IO是C语言提供的库函数,均以f打头;
		fopen,fclose,fseek,fflush,fread,fwrite,fprintf,fscanf;

	直接IO:是Linux系统的API,操作系统的API也是C语言写的;
		open,close,lseek,fsync,read,write,pread,pwrite;

	应用程序内存:是通常写代码用 malloc/free,new/delete 等分配出来的内存;
	用户缓冲区:C语言FILE结构体里面 buffer;
	内核缓冲区:Linux操作系统的Page Cache。为了加快磁盘IO,Linux系统会把磁盘上的数据以 Page为单位在操作系统的内存里,这里的Page是Linux
			  系统定义的一个逻辑概念,一般一个Page为4k。

	对于缓冲IO,一般读操作会有3次数据拷贝,一个写操作,有反向的3次数据拷贝;
		读:磁盘 => 内核缓冲区 => 用户缓冲区 => 应用程序内存;
		写:应用程序内存 => 用户缓冲区 => 内核缓冲区 => 磁盘;

	对于直接IO:一个读操作,会有2次数据拷贝,一个写操作,有反向的2次数据拷贝;
		读:磁盘 => 内核缓冲区 => 应用程序内存;
		写:应用程序内存 => 内核缓冲区 => 磁盘。

	所谓的直接IO,其中直接的意思是指没有用户级缓冲,但操作系统本身的缓冲还是有的。

	关于缓冲IO和直接IO,有几点需要特别说明:
		1.fflush和fsync的区别。fflush是缓冲IO的一个API,它只是把数据从用户缓冲区刷到内核缓冲区,fsync则是把数据从内核缓冲区刷到磁盘。
		这意味着无论是缓冲IO,还是直接IO,如果在写数据之后不调用fsync,此时系统断电重启,最新的部分数据就会丢失,因为数据还只是在内核缓冲
		区中,操作系统还没来得及刷到磁盘。后面讲到数据库,数据一致性,这个fsync很重要。
		2.对于直接IO,也有read/write和pread/pwrite两组不同的API。pread/pwrite在多线程读写同一个文件的时候很有用。

4.2 内存映射文件与零拷贝 
	4.2.1 内存映射文件 
		相比于直接IO,内存映射文件更进一步了。当用户空间不再有物理内存,直接拿应用程序的逻辑内存映射到Linux操作系统的内核缓冲区,应用程序
	虽然读写的是自己的内存,但这个内存只是一个"逻辑地址",实际读写的是内核缓冲区。

		数据拷贝从缓冲IO的3次,到直接IO的2次,再到内存映射文件的1次。
			读:磁盘 => 内核缓冲区
			写:内核缓冲区 => 磁盘

		在Linux中,内存映射文件对应的系统API是:mmap()

	4.2.2 零拷贝 
		当用户需要把文件中的数据发送到网络的时候,如果不用零拷贝,实现方式有:

			实现方法1:利用直接IO,伪代码如下:
				fd1 = 打开的文件描述符
				fd2 = 打开的socket描述符
				buffer = 应用程序内存
				read(fd1,buffer ...) // 先把数据从文件中读取出来
				write(fd2,buffer ...) //再通过网络发送出去

			整个过程会有4次数据拷贝,读进来2次,写回去2次。
			磁盘 => 内核缓冲区 => 应用程序内存 => Socket缓冲区 => 网络

			实现方法2:利用内存映射文件,伪代码如下:
				fd1 = 打开的文件描述符
				fd2 = 打开的socket描述符
				buffer = 应用程序内存
				mmap(fd1,buffer ...) //先把磁盘数据映射到buffer上
				write(fd2,buffer ...) //再通过网络发送出去

			整个过程会有3次数据拷贝,不再经过应用程序内存,直接在内核空间中从内核缓冲区拷贝到socket缓冲区。

			实现方法3:零拷贝
			如果使用零拷贝,可能连内核缓冲区到socket缓冲区的拷贝也省略了。在内核缓冲区和socket缓冲区之间并没有做数据的拷贝,只是一个
		地址的映射,底层的网卡驱动程序要读取数据并发送到网络的时候,看似读的是socket缓冲区的数据,实际上直接读的是内核缓冲区的数据。

			在这里,虽然叫零拷贝,实际是2次数据拷贝,1次是从磁盘到内核缓冲区,1次是从内核缓冲区到网络。之所以叫零拷贝,是从内存的角度来看,
		数据在内存中没有发生过数据拷贝,只是在内存和IO之间传输。

			在Linux系统中,API是:sendfile()。


4.3 网络I/O模型 
	4.3.1 实现层面的网络I/O模型 
		Linux 语境下面的IO模型:
			1. 同步阻塞IO
				就是Linux的read和write函数,在调用的时候会被阻塞,直到数据读取完成,或者写入成功。

			2. 同步非阻塞IO
				和同步阻塞IO的API是一样的,只是打开fd的时候会带有 O_NONBLOCK 参数。于是,当调用read和write函数的时候,如果没有准备好
			数据,会立即返回,不会阻塞,然后让应用程序不断的去轮询。

			3. IO多路复用
				前面两种IO都只能用于简单的客户端开发。但对服务器程序来说,需要处理很多的fd(连接数可以达到几十万甚至百万)。如果都使用同步
			阻塞IO,要处理这么多fd需要非常多的线程,每一个线程处理一个fd;如果使用非阻塞IO,要应用程序轮询这么大规模的fd。这两种办法都
			不行,于是有了IO多路复用。

				在Linux中,有三种IO多路复用的办法:select,poll,epoll。如 select(),该函数是阻塞调用,一次性把所有的fd传进去,当有fd
			可读或者可写的时候,该函数会返回,返回结果也在这个函数的参数里面,告知应用程序哪些fd上面可读可写,然后下一步应用程序调用read和
			write函数进行数据读写。

			4. 异步IO
				windows的异步IO是 IOCP。所谓异步IO是指读写都是由操作系统完成的,然后通过回调函数或者某种其他通信机制通知应用程序。

		总结:
			1.阻塞和非阻塞是从函数调用的角度来说的,而同步和异步是从"读写是谁完成的"角度来说的。
				阻塞:如果读写没有就绪或者读写没有完成,则该函数一直等待。
				非阻塞:函数立即返回,然后应用程序轮询。
				同步:读写由应用程序完成。
				异步:读写由操作系统完成,完成之后,通过回调或者事件通知应用程序。

			2.按照这个定义可以知道,异步IO一定是非阻塞IO,不存在既是异步IO,又是阻塞IO的;同步IO可能是阻塞的,也可能是非阻塞的。归类后
			总共有3种:同步阻塞IO,同步非阻塞IO,异步IO。

			3.IO多路复用(select,poll,epoll)都是同步IO,因为read和write函数都是应用程序完成的,同时也是阻塞IO,因为select,poll,
			epoll的调用都是阻塞的。

			所以,当讲网络IO模型的时候,一定要注意讲的是操作系统层面的IO模型,还是上层的网络框架封装出来的IO模型(如asio,如Java的NIO,
		在Linux平台,底层也是基于epoll)。

			另外,对于异步IO一词,在操作系统的语境和上层应用的语境中,往往指代不一样。在操作系统的语境中,异步IO是指IOCP或者aio这种真正
		的异步IO,epoll不被认为是异步IO;但在上层语境中,异步IO往往是指 JavaJDK或者网络框架(Netty)封装出来的概念,底层实现可能
		epoll,也可能是真正的异步IO。

	4.3.2 Reactor模式与Preactor模式 
		Reactor模式与Preactor模式,它们是网络框架的两种设计模式。
			1.Reactor模式。主动模式。所谓主动模式,是指应用程序不断的轮询,询问操作系统或者网络框架,IO是否准备就绪。在Linux系统下的
			select,poll,epoll就属于主动模式,需要应用程序中有一个循环一直轮询;Java中的NIO也属于这种模式。在这种模式下,实际的IO
			操作还是应用程序执行。

			2.Proactor模式。被动模式。应用程序把read和write函数操作全部交给操作系统或者网络框架,实际的IO操作由操作系统或者网络框架
			完成,之后再回调应用程序。asio库就是典型的Proactor模式。

	4.3.3 select、epoll的LT与ET 
		1.select 
			int select(int maxfdp1, fd_set* readfds, fd_set writefds, fd_set* execptfds, struct timeval* timeout);

			说明:
			1.因为fd是一个int值,所以fd_set其实是一个bit组数组,每1位表示一个fd是否有读写事件。
			2.第一个参数是readfds或者writefds的下标的最大值+1。因为fd从0开始,+1才表示个数。
			3.返回结果还是在readfds或者writefds里面,操作系统会重置所有bit位,告知应用程序到底哪个fd上面有事件,应用程序需要自己从0到
			maxfds - 1遍历所有的fd,然后执行相应的read/write操作。
			4.每次select调用返回后,下一次调用之前,要重新维护readfds和writefds。

		2.poll
			int poll(struct pollfd* fds, unsigned int nfds, int timeout);
			struct pollfd {
				int fd;
				short events; //每个fd,两个bit数组,一个进去,一个出来
				short revents;
			};

			从上面可以看出来,select,poll 每次调用都需要应用程序把fd的数组传进去,这个fd的数组每次都要在用户态和内核态之间传递,影响
		效率。为了,epoll设计了"逻辑上的epfd"。epfd是一个数字,把fd数组关联到上面,然后每次向内核传递的是epfd这个数字。

		3.epoll
			//创建一个epoll句柄,size告诉内核监听的数目一共有多少。其中的size并不要求是准确的数字,只是告诉内核,计划监听多少个fd。实际
			//通过epoll_ctl添加的fd数目可能大于这个值。
			int epoll_create(int sieze);

			//将一个fd增/删到epfd里,对应的事件也即读/写
			int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event);

			//其中的maxevents也是可以自定义的,假如有100个fd,而maxevents只设置64,则其他fd上面的事件会在下次调用epoll_wait时返回
			int epoll_wait(int epfd, struct_epoll_event* events, int maxevents, int timeout);

			整个epoll的过程分为三个步骤:
				1.事件注册
					通过函数epoll_ctl实现。对服务器而言,是accept,read,write三种事件;对客户端而言,是connect,read,write。

				2.轮询这3个事件是否就绪
					通过函数epoll_wait实现。有事件发生,该函数就返回。

				3.事件就绪,执行实际的IO操作
					通过函数 accept/read/write实现。

			事件就绪:
				1.read事件就绪
					远程有新数据来了,socket读写缓存区里有数据,需要调用read函数处理。

				2.write事件就绪
					是指本地的socket写缓冲区是否可写。如果写缓冲区没有满,则一直是可写的,write事件是就绪的,可以调用write函数。只有
				当遇到发送大文件的场景,socket写缓冲区被占满时,write事件才不是就绪的状态。

				3.accept事件就绪
					有新的连接进入时,需要调用accept函数处理。

		4.epoll的ET和LT模式
			epoll里面有两种模式:LT(水平触发)和ET(边缘触发)。水平触发又称为条件触发,边缘触发又称为状态触发。

			水平触发:
				读缓冲区主要不为空,就会一直触发读事件;写缓冲区只要不满,就会一直触发写事件。

			边缘触发:
				读缓冲区的状态,从空转为非空的时候触发一次;写缓冲区的状态,从满转为非满的时候,触发一次。比如用户发送一个大文件,把
			写缓冲区塞满后,之后缓冲区可以写了,就会发生一次从满到不满的切换。

			关于LT和ET,有两种需要注意的问题:
				1.对于LT模式,要避免"写的死循环"问题:写缓冲区为满的概率很小,即"写的条件"会一直满足,所以当用户注册了写事件却没有数据要写
				的时候,它会一直触发,因此在LT模式下写完数据一定要取消写事件。
				2.对于ET模式,"要避免 short read"问题。例如用户收到100个字节,它触发了1次,但用户只读取了50个字节,剩下的50个不读,它
				也不会再次触发。因此在ET模式下,一定要把"读缓冲区"的数据一次性读完。

			在实际开发中,一般倾向于用LT,这也是默认的模式。因为ET容易漏事件,一次触发如果没有处理好,就没有第二次机会。虽然LT重复触发看似
		有少许的性能损耗,但代码写起来更安全。

	4.3.4 服务器编程的1 N M模型 
		在服务器的编程中,epoll 的编程步骤是由不同的线程负责的,即服务器编程的 1+N+M模型。

		整个服务器有 1+N+M个线程,1个监听线程,N个IO线程,M个Worker线程。N的个数通常等于cpu核数,M的个数更具上层决定,通常有几百个。
			1.监听线程
				负责accept事件的注册和处理。和每一个新来的客户端建立socket连接,然后把socket连接转交给IO线程,完成任务,继续监听新的。

			2.IO线程
				负责每个socket连接上面read/write事件的注册和实际的socket读写。把读到的Request放入Request队列,交由Worker线程处理。

			3.Worker线程
				纯粹的业务线程,没有socket读写。对Request队列进行处理,生成Response队列,然后写入Response队列,由IO线程再回复给客户端。

		Tomcat6的NIO网络模型:
			IO线程只负责read/write事件的注册和监听,执行了epoll里面的前面2个阶段,第三个阶段是在Worker线程里面做的。IO线程监听到一个
		socket连接上有读事件,于是把socket转交给Worker线程,worker线程读出数据,处理完业务逻辑,直接返回给客户端。之所以可以这么做,是
		因为IO线程已经检测到读事件就绪,所以当worker线程在读的时候不会等待。IO线程和worker线程之间交互,不再需要一来一回两个队列,直接
		是一个socket集合。

4.4 进程、线程和协程 
	用Java的人通常写的是"单进程多线程"的程序;而用C++的人,可能写的是"单进程多线程","多进程单线程","多进程多线程"的程序。之所以会有这样的
差异,是因为Java程序并不是直接跑在Linux系统上的,而是运行在JVM之上,而一个JVM实例是Linux进程,每一个JVM都是一个独立的"沙盒",JVM之间互相
独立,互不通信。所以Java程序只能在这一个进程里面,开多个线程开发。而C++直接运行在Linux系统上,可以利用Linux系统提供的强大的进程间的通信机制
(IPC),很容易创造出多个进程,并实现进程间的通信。
	
	1.为什么要多线程
		多线程主要是为了应对IO密集型的应用。多线程能带来两方面的好处:
			1.提供cpu利用率
			2.提供IO吞吐

	2.多进程
		多线程存在两个问题:
			1.线程间内存共享,要加线程锁;而加锁后会导致并发效率下降,同时复杂的加锁机制也导致将增加编码的难度;
			2.过多的线程会造成线程间的上下文切换,导致效率低下。

		在并发编程领域,有一个很重要的原则:"不要通过共享内存来实现通信,而应通过通信实现共享内存"。通俗点就是:"尽可能通过消息通信,而不是
	共享内存来实现进程或线程之间的同步"。

		进程是资源分配的基本单位,进程间不共享资源,通过管道或者socket方式通信(当然也可以共享内存),这种通信方式天生符合上面的并发设计原
	则。而对于多线程,大家习惯于共享内存,然后通过加各种锁来实现同步。

		除了锁的问题,多进程来带来的另外2个好处是:
			1.一是减少了多线程在不同的cpu核间切换的开销;
			2.多进程互相独立,意味着一个崩溃了,其他进程可以继续运行。

		有了多进程之后,每个进程内部,可能是单线程,也可能是多线程,这往往取决于IO。

		对于IO密集型的应用,要提高IO效率,则需要下面几种办法:
			1.异步IO
				异步化后,请求可以Pipeline处理,就不需要多线程了。但像mysql的JDBC提供的都是同步接口,不支持异步IO。

			2.多线程
				IO不支持异步,就只能开多个线程,每个线程都同步的调用IO,实际上是用多线程模拟了异步IO。如web应用服务器调用redis/mysql。

			3.多协程

	3.多协程
		多线程除锁之外,还有一个问题是线程太多,切换的开销很大。虽然线程切换的开销比进程切换的开销已经小了很多,但还是不够。以
	  tomcat为例,在通常配置的服务器最多只能开几百个线程。如果再多,则线程切换的开销太大,并发效率反而会下降。这意味着tomcat最多
	  能并发的接受几百个请求。但如果是协程的话,可以开几万个。协程相比线程有两个关键特点:
		a) 更好的利用cpu。线程的调度是操作系统完成的,应用程序干预不了,协程可以由应用程序自己调度。
		b) 更好的利用内存。协程的堆栈大小不是固定的,用多少申请多少,内存利用率更高。

4.5 无锁(内存屏障与CAS) 
	4.5.1 内存屏障 
		Linux 内核的 kfifo.c 源码实现了无锁。核心的要点是:
			1.读可以多线程,写必须是单线程,也称为 Single-Writer Principle。如果是多线程写,则做不到无锁。
			2.在上面的基础上,使用了内存屏障。也就是 smp_wmb()调用。从用法来说,内存屏障是在两行代码之间插入一个栅栏,如下所示:
				代码第1行
				代码第2行
					内存屏障
				代码第3行
				代码第4行

			在第2行代码和第3行代码之间插入一个内存屏障,这样前2行代码就不会跑到后2行代码的后面执行。虽然第1行,第2行之间可能被重排序;
		第3行和第4行可能被重排序,但第1行,第2行不会跑到第3行后面去。

			所谓的重排序,通俗的讲,就是cpu不会按照开发者的代码顺序来执行。

		基于内存屏障,有了Java中的 volatile 关键字,再加上单线程写的原则,就有了Java中无锁并发框架 --- Disruptor,其核心就是 "
	一写多读,完全无锁"。

	4.5.2 CAS 
		如果是多线程写,则内存屏障也不够用了,这时要用到CAS。CAS是cpu层面提供的一个硬件原子指令,实现对同一个值的Compare和Set两个
	操作的原子化。

		基于CAS,上层可以实现 乐观锁,无锁队列,无锁栈,无锁链表。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

热门文章

暂无图片
编程学习 ·

C语言二分查找详解

二分查找是一种知名度很高的查找算法,在对有序数列进行查找时效率远高于传统的顺序查找。 下面这张动图对比了二者的效率差距。 二分查找的基本思想就是通过把目标数和当前数列的中间数进行比较,从而确定目标数是在中间数的左边还是右边,将查…
暂无图片
编程学习 ·

GMX 命令分类列表

建模和计算操作命令: 1.1 . 创建拓扑与坐标文件 gmx editconf - 编辑模拟盒子以及写入子组(subgroups) gmx protonate - 结构质子化 gmx x2top - 根据坐标生成原始拓扑文件 gmx solvate - 体系溶剂化 gmx insert-molecules - 将分子插入已有空位 gmx genconf - 增加…
暂无图片
编程学习 ·

一文高效回顾研究生课程《数值分析》重点

数值分析这门课的本质就是用离散的已知点去估计整体,就是由黑盒子产生的结果去估计这个黑盒子。在数学里这个黑盒子就是一个函数嘛,这门课会介绍许多方法去利用离散点最大化地逼近这个函数,甚至它的导数、积分,甚至微分方程的解。…
暂无图片
编程学习 ·

在职阿里5年,一个28岁女软测工程师的心声

简单的先说一下,坐标杭州,14届本科毕业,算上年前在阿里巴巴的面试,一共有面试了有6家公司(因为不想请假,因此只是每个晚上去其他公司面试,所以面试的公司比较少) ​ 编辑切换为居中…
暂无图片
编程学习 ·

字符串左旋c语言

目录 题目: 解题思路: 第一步: 第二步: 第三步: 总代码: 题目: 实现一个函数,可以左旋字符串中的k个字符。 例如: ABCD左旋一个字符得到BCDA ABCD左旋两个字符…
暂无图片
编程学习 ·

设计模式--观察者模式笔记

模式的定义与特点 观察者(Observer)模式的定义:指多个对象间存在一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。这种模式有时又称作发布-订阅模式、模型-视图模式&#xf…
暂无图片
编程学习 ·

睡觉突然身体动不了,什么是睡眠痽痪症

很多朋友可能有这样的体验,睡觉过程中突然意识清醒,身体却动弹不了。这时候感觉非常恐怖,希望旁边有一个人推自己一下。阳光以前也经常会碰到这样的情况,一年有一百多次,那时候很害怕晚上到来,睡觉了就会出…
暂无图片
编程学习 ·

深入理解C++智能指针——浅析MSVC源码

文章目录unique_ptrshared_ptr 与 weak_ptrstd::bad_weak_ptr 异常std::enable_shared_from_thisunique_ptr unique_ptr 是一个只移型别(move-only type,只移型别还有std::mutex等)。 结合一下工厂模式,看看其基本用法&#xff…
暂无图片
编程学习 ·

@TableField(exist = false)

TableField(exist false) //申明此字段不在数据库存在,但代码中需要用到它,通知Mybatis-plus在做写库操作是忽略它。,.
暂无图片
编程学习 ·

Java Web day15

第十二章文件上传和下载 一、如何实现文件上传 要实现Web开发中的文件上传功能,通常需要完成两步操作:一.是在Web页面中添加上传输入项;二是在Servlet中读取上传文件的数据,并保存到本地硬盘中。 需要使用一个Apache组织提供一个…
暂无图片
编程学习 ·

【51nod 2478】【单调栈】【前缀和】小b接水

小b接水题目解题思路Code51nod 2478 小b接水 题目 输入样例 12 0 1 0 2 1 0 1 3 2 1 2 1输出样例 6解题思路 可以发现最后能拦住水的都是向两边递减高度(?) 不管两个高积木之间的的积木是怎样乱七八糟的高度,最后能用来装水的…
暂无图片
编程学习 ·

花了大半天写了一个UVC扩展单元调试工具

基于DIRECTSHOW 实现的,用的是MFC VS2019. 详见:http://www.usbzh.com/article/detail-761.html 获取方法 加QQ群:952873936,然后在群文件\USB调试工具&测试软件\UVCXU-V1.0(UVC扩展单元调试工具-USB中文网官方版).exe USB中文网 USB中文…
暂无图片
编程学习 ·

贪心(一):区间问题、Huffman树

区间问题 例题一:区间选点 给定 N 个闭区间 [ai,bi]请你在数轴上选择尽量少的点,使得每个区间内至少包含一个选出的点。 输出选择的点的最小数量。 位于区间端点上的点也算作区间内。 输入格式 第一行包含整数 N,表示区间数。 接下来 …
暂无图片
编程学习 ·

C语言练习实例——费氏数列

目录 题目 解法 输出结果 题目 Fibonacci为1200年代的欧洲数学家,在他的着作中曾经提到:「若有一只免子每个月生一只小免子,一个月后小免子也开始生产。起初只有一只免子,一个月后就有两只免子,二个月后有三只免子…
暂无图片
编程学习 ·

Android开发(2): Android 资源

个人笔记整理 Android 资源 Android中的资源,一般分为两类: 系统内置资源:Android SDK中所提供的已经定义好的资源,用户可以直接拿来使用。 用户自定义资源:用户自己定义或引入的,只适用于当前应用的资源…
暂无图片
编程学习 ·

零基础如何在短时间内拿到算法offer

​算法工程师是利用算法处理事物的职业 算法(Algorithm)是一系列解决问题的清晰指令,也就是说,能够对一定规范的输入,在有限时间内获得所要求的输出。 如果一个算法有缺陷,或不适合于某个问题,执…
暂无图片
编程学习 ·

人工智能:知识图谱实战总结

人工智能python,NLP,知识图谱,机器学习,深度学习人工智能:知识图谱实战前言一、实体建模工具Protegepython,NLP,知识图谱,机器学习,深度学习 人工智能:知识图…
暂无图片
编程学习 ·

【无标题】

这里写自定义目录标题欢迎使用Markdown编辑器新的改变功能快捷键合理的创建标题,有助于目录的生成如何改变文本的样式插入链接与图片如何插入一段漂亮的代码片生成一个适合你的列表创建一个表格设定内容居中、居左、居右SmartyPants创建一个自定义列表如何创建一个注…