现在操作系统都采用虚拟寻址,处理器先产生一个虚拟地址,通过地址翻译成物理地址(内存的地址),再通过总线的传递,最后处理器拿到某个物理地址返回的字节。
对 32 位操作系统而言,它的寻址空间(虚拟存储空间)为 4G(2 的 32 次方)。
核空间:在 liunx 中,将最高的 1G 字节(从虚拟地址 0xC0000000 到 0xFFFFFFFF),供内核使用,称为“内核空间”。
用户空间:在 liunx 中,将较低的 3G 字节(从虚拟地址 0x00000000 到 0xBFFFFFFF),供各个进程使用,称为“用户空间)。
内核态:当一个任务(进程)执行系统调用而陷入内核代码中执行时,我们就称进程处于内核运行态
用户态:每个进程都有自己的内核栈。当进程在执行用户自己的代码时,则称其处于用户运行态(用户态)
因为每个进程可以通过系统调用进入内核,因此,Linux 内核由系统 内的所有进程共享。于是,从具体进程的角度来看,每个进程可以拥有 4G 字节的虚拟空间。
为了控制进程的执行,内核必须有能力挂起正在 CPU 上运行的进程,并恢复以前挂起的某个进程的执行。这种行为被称为进程切换(也叫调度)。因此可以说,任何进程都是在操作系统内核的支持下运行的,是与内核紧密相关的。
从一个进程的运行转到另一个进程上运行,这个过程中经过下面这些变化:
这些值包括描绘地址空间的页表、包含进程相关信息的进程表、文件表等。
可以理解成一个比较耗资源的过程。
缓存 I/O 又被称作标准 I/O,大多数文件系统的默认 I/O 操作都是缓存 I/O。在 Linux 的缓存 I/O 机制中,以 write 为例,数据会先被拷贝进程缓冲区,在拷贝到操作系统内核的缓冲区中,然后才会写到存储设备中。
对于一次 IO 访问(这回以 read 举例),数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的缓冲区,最后交给进程。所以说,当一个 read 操作发生时,它会经历两个阶段:
正式因为这两个阶段,linux 系统产生了下面五种网络模式的方案:
read 为例:
(1)进程发起 read,进行 recvfrom 系统调用;
(2)内核开始第一阶段,准备数据(从磁盘拷贝到缓冲区),进程请求的数据并不是一下就能准备好;准备数据是要消耗时间的;
(3)与此同时,进程阻塞(进程是自己选择阻塞与否),等待数据 ing;
(4)直到数据从内核拷贝到了用户空间,内核返回结果,进程解除阻塞。
也就是说,内核准备数据和数据从内核拷贝到进程内存地址这两个过程都是阻塞的。
可以通过设置 socket 使其变为 non-blocking。当对一个 non-blocking socket 执行读操作时,流程是这个样子:
(1)当用户进程发出 read 操作时,如果 kernel 中的数据还没有准备好;
(2)那么它并不会 block 用户进程,而是立刻返回一个 error,从用户进程角度讲,它发起一个 read 操作后,并不需要等待,而是马上就得到了一个结果;
(3)用户进程判断结果是一个 error 时,它就知道数据还没有准备好,于是它可以再次发送 read 操作。一旦 kernel 中的数据准备好了,并且又再次收到了用户进程的 system call;
(4)那么它马上就将数据拷贝到了用户内存,然后返回。
所以,nonblocking IO 的特点是用户进程在内核准备数据的阶段需要不断的主动询问数据好了没有。
I/O 多路复用实际上就是用 select, poll, epoll 监听多个 io 对象,对一个 IO 端口,两次调用,两次返回,比阻塞 IO 并没有什么优越性;关键是能实现同时对多个 IO 端口进行监听;当 io 对象有变化(有数据)的时候就通知用户进程。现在先来看下 I/O 多路复用的流程:
(1)当用户进程调用了 select,那么整个进程会被 block;
(2)而同时,kernel 会“监视”所有 select 负责的 socket;
(3)当任何一个 socket 中的数据准备好了,select 就会返回;
(4)这个时候用户进程再调用 read 操作,将数据从 kernel 拷贝到用户进程。
所以,I/O 多路复用的特点是通过一种机制一个进程能同时等待多个文件描述符,而这些文件描述符(套接字描述符)其中的任意一个进入读就绪状态,select() 函数就可以返回。
这个图和 blocking IO 的图其实并没有太大的不同,事实上,还更差一些。因为这里需要使用两个 system call (select 和 recvfrom),而 blocking IO 只调用了一个 system call (recvfrom)。 但是,用 select 的优势在于它可以同时处理多个 connection。
所以,如果处理的连接数不是很高的话,使用 select/epoll 的 web server 不一定比使用多线程 + 阻塞 IO 的 web server 性能更好,可能延迟还更大。
select/epoll的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。
在 IO multiplexing Model 中,实际中,对于每一个 socket,一般都设置成为 non-blocking,但是,如上图所示,整个用户的 process 其实是一直被 block 的。只不过 process 是被 select 这个函数 block,而不是被 socket IO 给 block。
我们首先开启套接口的信号驱动 I/O 功能,并通过 sigaction 系统调用安装一个信号处理函数。该系统调用立即返回,我们的进程继续工作,也就是说它没有被阻塞。当数据报准备好读取时,内核就为该进程产生一个 SIGIO 信号。我们随后既可以在信号处理函数中调用 recvfrom 读取数据报,并通知主循环数据已准备好待处理,也可以立即通知主循环,让它读取数据报。
无论如何处理 SIGIO 信号,这种模型的优势在于等待数据报到达期间,进程不被阻塞。主循环可以继续执行,只要不时等待来自信号处理函数的通知:既可以是数据已准备好被处理,也可以是数据报已准备好被读取。
异步 I/O(asynchronous I/O)由 POSIX 规范定义。一般地说,这些函数的工作机制是:告知内核启动某个操作,并让内核在整个操作(包括将数据从内核拷贝到我们自己的缓冲区)完成后通知我们。这种模型与信号驱动模型的主要区别在于:信号驱动 I/O 是由内核通知我们何时启动一个 I/O 操作,而异步 I/O 模型是由内核通知我们 I/O 操作何时完成。
我们调用 aio_read 函数(POSIX 异步 I/O 函数以 aio_或 lio_开头),给内核传递描述字、缓冲区指针、缓冲区大小(与 read 相同的三个参数)、文件偏移(与 lseek 类似),并告诉内核当整个操作完成时如何通知我们。该系统调用立即返回,在等待 I/O 完成期间,我们的进程不被阻塞。
(1)用户进程发起 read 操作之后,立刻就可以开始去做其它的事。
(2)而另一方面,从 kernel 的角度,当它受到一个 asynchronous read 之后,首先它会立刻返回,所以不会对用户进程产生任何 block。
(3)然后,kernel 会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,kernel 会给用户进程发送一个 signal,告诉它 read 操作完成了。
首先前文已述 I/O 多路复用的本质就是用 select/poll/epoll,去监听多个 socket 对象,如果其中的 socket 对象有变化,只要有变化,用户进程就知道了。
select 是不断轮询去监听的 socket,socket 个数有限制,一般为 1024 个;
poll 还是采用轮询方式监听,只不过没有个数限制;
epoll 并不是采用轮询方式去监听了,而是当 socket 有变化时通过回调的方式主动告知用户进程。
require 'socket'
one_hundred_kb = 1024 * 100
Socket.tcp_server_loop(4481) do |connection|
begin
# readpartial(1) It blocks only if ios has no data immediately available.
while data = connection.readpartial(one_hundred_kb) do
puts data
end
rescue EOFError # EOFError Raised by some IO operations when reaching the end of file.
end
connection.close
end
IO.select 会阻塞并且轮询 socket
read
require 'socket'
Socket.tcp_server_loop(4481) do |connection|
loop do
begin
puts connection.read_nonblock(4096)
rescue Errno::EAGAIN
IO.select([connection])
retry
rescue EOFError
break
end
end
connection.close
end
wirte
require 'socket'
client = TCPSocket.new('localhost',4481)
payload = 'hihihihi' * 10_00
begin
loop do
bytes = client.wirte_nonblock(payload)
break if bytes >= payload.size
payload.slice!(0,bytes)
IO.select(nil,[client])
end
rescue Errno::EAGAIN
IO.select(nil,[client])
retry
end
除了非阻塞读写,server 端有非阻塞接受 accept_nonblock。client 有非阻塞连接 connect_nonblock
select 版本
connections = [<TCPSocket>,<TCPSocket>,<TCPSocket>]
loop do
ready = IO.select connections
readable_connections = ready[0]
readable_connections.each do |conn|
begin
conn.read_nonblock(4096)
rescue Errno::EAGAIN
end
end
end
``
使用IO.select 可以减低无用连接的开销。