本质论-Unix系统I/O

简单总结一下学习系统IO的内容,本文只涉及如何使用系统IO,不涉及内部的实现。

为什么需要I/O?

I/O是 解决如何在外部设备和内存之间交换数据的问题,也就是如何从外部设备上读取数据到内存中,以及如何把内存中数据写入到外部设备中去。最常见的外部设备就是硬盘,我们经常需要读写里面存储的文件。一个文件可以看成是一个m字节的序列,内存中的数据也是一些字节序列(也可以认为是字节数组)。因此,I/O的基本模型可以看成是在两个字节序列间拷贝数据。

Unix中最基本的I/O操作包括了以下这些函数(或系统调用):
// 打开一个文件,并获得其文件描述符。文件描述符可以认为是指向一个文件的指针,打开一个文件后获得指向该文件的描述符,后续的操作都是基于这个描述符的。
int open(char* filename, int flags, int mode);

// 读取文件中的内容, 从fd指定的文件中拷贝最多n个字节并存放到由buf指向的内存中。
ssize_t read(int fd, void buf*, size_t n)

// 写文件,将buf指向的内存中拷贝n个字节到由fd指定文件中
ssize_t write(int fd, const void buf*, size_t n);

// 关闭一个文件
int close (int fd);
以上就是系统I/O中最基本的函数,可见接口相当简洁,这符合系统设计中最小接口原则。其他的I/O库本质上都是基于上面这些函数来实现的。比如:
  1. 上面的函数都是系统调用,每次调用都会进入内核模式,调用的成本会很高,那么就可以考虑在其基础上加上缓存,以减少系统调用的次数。类似于Java中的BufferedInputStream和普通的FileInputStream的关系。
  2. 上面的函数读写的单位都是字节。如果读写比较复杂的数据结构,就不是很方便,因此可以在其基础上开发出更友好的文件访问函数,比如标准库就提供了格式化的输入和输出,按行读取内容。Java中的ObjectInputStream就提供了读取对象的方法。
了解了他们的本质就不会对其他库函数感到困惑了。

统一的接口

很多人是在学习了面向对象编程后了解了设计模式,但是设计模式所体现的思想并不是面向对象编程语言的所特有的,而是普遍存在的。比如,面向接口编程,Unix的系统I/O就是一个很好的例子。在Unix中,所有的外部设备,网络,磁盘,屏幕都被建模为文件,这样使用者就可以用读写文件的方式去读写这些设备了。比如,需要从键盘读取输入,就可以从文件描述符为0的这个“文件”中读取;需要往屏幕上输出内容,就可以往文件描述符为1的这个“文件”中写入内容。按照面向接口编程的思想,可以动态地替换接口的实现而无需修改使用接口的代码。在Unix中,可以通过重定向将标准输入和标准输出指向某个文件。

Socket网络编程

另外一个和Unix系统I/O很相关的例子就是Socket网络编程。Socket的本质是在两台主机之间以类似于文件操作的方式交换数据的API。

客户端

客户端用如下函数来创建一个类似于文件描述符的socket描述符
int socket(int domain, int type, int protocol)
不过,上面创建的描述符只是部分打开的,还不能用于读写,还需要用connect函数来真正建立和服务器端的连接:
int connect(int socketfd, struct sockaddr *server_addr, int addr_length);
这样,你就可以向操作文件一样去和网络上的服务器进行数据交换了。

服务器端

服务器端要稍微复杂一点,可以分成3步:
1. 首先,用socket函数来创建一个socket描述符。然后用bind方法将socket描述符和一个服务器地址关联起来,这样内核就知道通过某个端口来的请求是要发到这个描述符上。
int bind(int socketfd, struct sockaddr *myaddr, int addrlen
2. 用socket方法创建的描述符默认都是主动套接字,也就是主动发起连接请求的,它是在客户端使用的。对于服务器端,我们需要调用listen方法来告诉内核这是一个监听套接字,从而它能接受来自客户端的请求。
int listen(int socketfd, int backlog);
3. 最后再调用accept方法等待客户端的连接请求。如果没有连接请求,服务器端将阻塞在accept方法上,一旦连接请求到达,它将返回一个已连接的套接字描述符(connected descriptor)。
accept的方法签名是:
int accept(int listenfd, struct sockaddr *addr, int addrlengh);
在获得已连接的套接字描述符后,服务器端就可以用前面提到的read来读取客户端发来的数据,和用write来发送响应客户端。

需要注意的是,客户端只需一个socket描述符,一旦和服务器段建立连接后就用这个描述符和服务器交换数据。而服务器端有两个描述符,一个是用来处理连接请求的(socket方法创建的那个描述符),另外一个是用来做数据通信的(accept方法返回的那个描述符)。这样可以使得我们可以创建并发服务器,能同时处理多个客户端的连接

小结

简单总结一下,Unix系统IO最基本的函数就是read和write,所有的外部设备都被建模为文件,使得我们可以像读写文件一下读写外设。Socket的本质是通过connect,bind,listen和accept来创建Socket描述符,从而使得我们能像读写本地文件一下去和远端服务器通信。各种应用层的协议(比如HTTP)的本质也就是通过read和write按照协议的要求传输指定格式的数据。
也许你已经发现本文中经常提到“本质”这个词。我认为任何复杂系统的本质其实很简单,复杂的是要让系统适应各种复杂的环境和需求,比如各种异常处理。掌握了系统的本质有助于理解系统的架构,更能理解各个模块之间是如何在一起协调工作的,在处理具体的问题的时候才知道当前工作这一块是为系统的哪部分服务。

你可能感兴趣的:(本质论-Unix系统I/O)