Io

    关于网络IO中的同步、异步、阻塞、非阻塞 ==================== 在高并发编程当中,我们经常会遇到一些异步、非阻塞等一些概念,一些常用的技术比如异步的httpclient、netty nio、nginx、node.js等,它们的原理大都跟异步、非阻塞有关。特别是在服务器开发中,并发的请求处理是个大问题,阻塞式的函数会导致资源浪费和时间延迟。通过事件注册、异步函数,开发人员可以提高资源的利用率,性能也会改善。其nginx和node.js处理并发都是采用的事件驱动异步非阻塞模式。其中nginx中处理并发用的是epoll,poll,queue等方式,node.js使用的是libev,它们对大规模的HTTP请求处理的都很好。 那么到底什么是异步、非阻塞,它们的原理是什么,它们之间又有什么区别呢?其实在很多情况下,异步与非阻塞(同步与阻塞)表示的是同一个意思,但是在特定的上下文环境中,它们含义又十分不同。再具体讲它们的区别之前,先介绍一下上下文背景。 一、上下文背景 我们所遇到的这些场景大部分都是当用户进程(或线程)在进行网络IO时即进行Socket读写时遇到的,所以本文讨论的上下文背景是基于Linux环境下的network IO。先介绍一下其中我们最常见的五种IO: 1. blocking IO 2. nonblocking IO 3. IO multiplexing 4. signal driven IO 5. asynchronous IO 由于signal driven IO在实际中并不常用,所以我这只提及剩下的四种IO Model。 再说一下IO发生时涉及的对象和步骤。对于一个network IO (这里我们以read举例),它会涉及到两个系统对象,一个是调用这个IO的进程(或线程),另一个就是系统内核(kernel)。当一个read操作发生时,它会经历两个阶段: 等待数据准备(Waiting for the data to be ready) 将数据从内核拷贝到进程中 (Copying the data from the kernel to the process) 记住这两点很重要,因为这些IO Model的区别就是在两个阶段上各有不同的情况。 二、各种IO介绍 2.1 blocking IO 在linux中,默认情况下所有的socket都是blocking,也就是说我们的一个进程在进行IO操作时如果没有数据达到,这个进程是被阻塞的。一个典型的读操作流程大概是这样: 当用户进程调用了recvfrom这个系统调用,kernel就开始了IO的第一个阶段:准备数据。对于network io来说,很多时候数据在一开始还没有到达(比如,还没有收到一个完整的UDP包),这个时候kernel就要等待足够的数据到来。而在用户进程这边,整个进程会被阻塞。当kernel一直等到数据准备好了,它就会将数据从kernel中拷贝到用户内存,然后kernel返回结果,用户进程才解除block的状态,重新运行起来。所以,blocking IO的特点就是在IO执行的wait和copy两个阶段都被block了。 在这种block IO的情况下,如果请求的连接比较多,但其中大部分都是阻塞的。因为cpu的核数是有限的,所以一般的解决方案就是每个cpu启用多个线程来处理多个连接。这种解决方案有很大的缺陷: 1. 线程是有内存开销的,1个线程可能需要512K(或2M)存放栈,那么1000个线程就要512M(或2G)内存 2. 线程的切换开销和很大,因为线程切换时需要保持当前线程上下文信息,当大量时间花在上下文切换的时候,分配给真正的操作的CPU就要少很多 3. 一个cpu所支持的线程数量时有限的(因为上面两个原因),一般来说线程的数量级在几百个左右就已经很大了 为了解决block IO存在的问题,就引入了no-blocking IO概念。

    fd

    在Linux通用I/O模型中,I/O操作系列函数(系统调用)都是围绕一个叫做文件描述符的整数展开。这不禁让人产生疑问:这个整数代表什么?一个数值代表一个文件吗?随便传一个整数进去调用可以吗? 解答以上疑问,需要更深入学习——文件描述符(File Descriptor)。 图解 理解具体情况,需要了解由内核维护的3个数据结构: 进程级文件描述符表(file descriptor table) 系统级打开文件表(open file table) 文件系统i-node表(i-node table) 这3个数据结构之间的关系如下图所示: ////——- 文件描述符表 内核为每个进程维护一个文件描述符表,该表每一条目都记录了单个文件描述符的相关信息,包括: 控制标志(flags),目前内核仅定义了一个,即close-on-exec 打开文件描述体指针 打开文件表 内核对所有打开的文件维护一个系统级别的打开文件描述表(open file description table),简称打开文件表。表中条目称为打开文件描述体(open file description),存储了与一个打开文件相关的全部信息,包括: 文件偏移量(file offset),调用read()和write()更新,调用lseek()直接修改 访问模式,由open()调用设置,例如:只读、只写或读写等 i-node对象指针 i-node表 每个文件系统会为存储于其上的所有文件(包括目录)维护一个i-node表,单个i-node包含以下信息: 文件类型(file type),可以是常规文件、目录、套接字或FIFO 访问权限 文件锁列表(file locks) 文件大小 等等 i-node存储在磁盘设备上,内核在内存中维护了一个副本,这里的i-node表为后者。副本除了原有信息,还包括:引用计数(从打开文件描述体)、所在设备号以及一些临时属性,例如文件锁。 场景解析 上图中,详细描述了两个进程诸多文件描述符,以及相互关系。 文件描述符复制 在进程A中,文件描述符1和文件描述符20都指向同一个打开文件描述体(标号23)。这很可能是通过调用dup()系列函数形成的。 文件描述符复制,在某些场景下非常有用,比如:标准输入/输出重定向。在shell下,完成这个操作非常简单,大部分人都会,但是极少人思考过背后的原理。 大概描述一下需要的几个步骤,以标准输出(文件描述符为1)重定向为例: 打开目标文件,返回文件描述符n; 关闭文件描述符1; 调用dup将文件描述符n复制到1; 关闭文件描述符n; 子进程继承文件描述符 进程A的文件描述符2和进程B的文件描述符2都指向同一个打开文件描述体(标号73)。这种情形很可能发生在调用fork()派生子进程之后,比如A调用fork()派生出B。这时,B作为子进程,从父进程A继承了文件描述符表,其中包括图中标明的文件描述符2。这就是子进程继承父进程打开的文件这句话的由来。

    encrypted

    一、网络数据传输安全概述 我们说的数据加密与解密通常是为了保证数据在网络传输过程中的安全性。在网络发展初期,网络的数据安全性是没有被足够的重视的。事实上,当时为了实现数据可以通过网络进行传输已经耗费了科学家大部分男细胞,因此在TCP/IP协议设计的初期,他们也实在没有太多精力去过多考虑数据在网络传输过程中可能存在的安全性问题。随着TCP/IP协议及相关技术的日渐成熟,网络数据传输技术越来越稳定,人们才慢慢开始重视这个问题,美国国家标准与技术研究院(National Institue of Standard and Technology,简称NIST)也开始制定相关的安全标准。 网络安全涉及到很多个方面,我们这里仅仅讨论下网络数据传输过程中可能受到的威胁,其中常见的有: 数据窃听 数据篡改 身份伪装 针对以上威胁,我们介绍下网络数据传输的安全性涉及的几个方面: 1. 机密性 机密性是指对要传输的数据进行加密和解密,防止第三方看到通信数据的明文内容。其对应的通信过程如下: 数据发送方: plaintext(明文) ==> 转换算法 ==> ciphertext(密文) 数据接收方: ciphertext(密文) ==> 转换算法 ==> plaintext(明文) 2. 完整性 数据完整性是指不允许数据在传输过程中被修改(第三方恶意篡改或电平信号造成的部分数据丢失),但是它不要求数据的机密性,也就是说允许其他人看到明文数据。我们通常通过以不可逆的算法对数据提取特征码(也叫数据指纹),通过验证特征码的一致性来判断数据是否被修改过,通信过程如下: 数据发送发: plaintext(明文) ==> 转换算法 ==> plaintext(明文) + footprint(数据指纹A) 数据接收方: plaintext(明文) + footprint(数据指纹A) ==> 转换算法 ==> footprint(数据指纹B) ==> 对比数据指纹A与B是否一致 3. 身份验证 身份验证通常是指数据接收方需要确认发送数据给自己的数据是自己想要通信的那一方,防止他人冒充通信对方的身份进行通信。身份验证的大体原理是:数据发送方与数据接收方约定一种特殊的数据加解密方式,数据发送方将一个通过约定的加密方式进行加密后的数据发送给数据接收方,数据接收方如能按照约定的加密方式正确解密该数据就表示对数据发送方的身份验证成功。其对应的通信过程如下: 数据发送方: plaintext(明文) ==> 转换算法 ==> ciphertext(密文) 数据接收方: ciphertext(密文) ==> 转换算法 ==> plaintext(明文) 二、数据加密算法分类 上面提到的网络数据传输所涉及到的几个方面都需要特定的转换算法来实现,常用的转换算法(数据加密/解密算法)大体上可以分为以下几类:

    Scale

    Scale 1英文字符(word,字符) = 2byte;[ASCII, Unicode, UTF-8] 1中文字符 = 2byte(B) [ASCII,Unicode]; 1中文字符 = 3byte(B) [UTF-8]; 1B(byte,字节) = 8 bit(见下文); 1KB(Kibibyte,千字节) = 1024B = 2^10 B; 1MB(Mebibyte,兆字节,百万字节,简称“兆”) = 1024KB = 2^20 B; 1GB(Gigabyte,吉字节,十亿字节,又称“千兆”) = 1024MB = 2^30 B; 1TB(Terabyte,万亿字节,太字节) = 1024GB = 2^40 B; 1PB(Petabyte,千万亿字节,拍字节) = 1024TB = 2^50 B; 1EB(Exabyte,百亿亿字节,艾字节) = 1024PB = 2^60 B; 1ZB(Zettabyte,十万亿亿字节,泽字节) = 1024EB = 2^70 B; 1YB(Yottabyte,一亿亿亿字节,尧字节) = 1024ZB = 2^80 B; 1BB(Brontobyte,一千亿亿亿字节) = 1024YB = 2^90 B;

    Lock

    #lock 悲观锁(Pessimistic Lock): 顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。 传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。 乐观锁(Optimistic Lock): 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。 乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库如果提供类似于write_condition机制的其实都是提供的乐观锁。 两种锁各有优缺点,不可认为一种好于另一种,像乐观锁适用于写比较少的情况下,即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。 但如果经常产生冲突,上层应用会不断的进行retry,这样反倒是降低了性能,所以这种情况下用悲观锁就比较合适。