linux文件IO操作

        说起linux编程来,甚至包括其他系统的编程,很大一部分都是处理IO操作了(另一个重要的部分是进程process)。特别是在linux系统上,由于一切都是文件(fd)的思想,更是扩大了文件IO的范畴。比如磁盘上文件的IO,网络IO,终端IO,管道IO等等。这些IO操作的共同点在于都是对文件描述符的操作,而且在最底层都可能是系统调用。当然如果我们考虑最多的两类IO,即文件IO和网络IO,它们的区别也很大。

       文件IO虽然也是系统调用,执行时会进入内核操作,但是往往速度是很快的,我们可以当成是一个普通函数的调用。对文件IO的操作其实我们很容易理解,全部就涉及到打开,读,写,关闭,定位到指定偏移量。对应的函数是open,read,write,close,lseek。值得注意的是这些操作都是系统调用,所以它们都是原子操作的,比如说write一段内容到某个文件,我们不必担心在此过程备中断而另一个任务去覆盖write。但是这些操作的组合确不是原子的,比如先lseek到某个位置然后write一段内容,这两个函数之间是有时间窗的,是有可能被中断的。

        文件IO在linux上是很底层的,所以linux又在上面做了一层封装,提出了标准IO的概念。标准IO引入了流(FILE对象),这些读写文件不再直接使用文件描述符(fd)了,而用一个FILE指针来操作文件,这有个好处是FILE对象是自带缓存区的,可以不必多次的进行系统调用。在c语言里,基本对文件的操纵都是通过FILE对象在操作的,在C++里更有文件IO的更高层封装,比如说fstream。思想上都是使用了流的概念。


        上图可以理解内核对文件IO准备的数据结构,此图摘自《UNIX环境高级编程》。每个进程表里有一个矢量(数组),数组的下标就是fd(所以fd其实是个短整数),每一项包含一个打开的文件fd的标志位(比如close_on_exec)以及一个指针(该指针指向文件表项);内核还为每个fd维持了一个文件表项,主要包括如图所示文件状态标志(读,写,append,同步,非阻塞等等),当前文件偏移量,v节点指针(注意,Linux系统没有v节点,直接指向的inode),另外注意,多个文件表项可能指向的同一个v节点,也就是实际打开的是同一个文件,但是他们的打开状态不同,偏移量也可能不同;最后内核为每个实际的文件准备了v节点和inode,inode真实的反映了该文件的长度,作者,在磁盘中的位置,block等等。

        总体上说来,文件IO的复杂度是不高的,基本认为读写都可以成功,而且是同步的阻塞式的读写,远远没有网络IO的复杂度高。网络IO我们这里只讨论TCP的操作,和文件IO类似,在应用层,对网络IO也是通过一个文件描述符来操作的,函数也很类似,比如也有open,read,write,close,但没有lseek,同时也多了一些操作函数,比如ioctl,fctnl等,由于得考虑双工通道的单向关闭,又多了shutdown这样的函数。说到底网络IO和文件IO的最大区别在于它是个“低速”的系统,是有可能阻塞调用进程的。正是有了这个区别,才导致网络IO异常的复杂,包括了同步,异步,阻塞,非阻塞等模式。为了提高效率,内核又有了select和epoll这样的系统调用来帮助我们。

        网络编程的操作对象也是文件描述符(fd),有一套称之为socket函数的操作来围绕这fd来处理网络IO。比如要连接对方的进程,那么必须本地先建立一个

        int fd = socket(AF_INET,SOCK_STREAM,0);

还要去连接对端地址,对端的地址表示由ip+port来决定

        sockaddr_in serv;

        serv.sin_famiy = AF_INET;

        serv.sin_port = ????;

        serv.sin_addr = ???;

        connect(fd, (sockaddr*)&serv,sizeof(serv));

这里都没有处理错误。光这些就很复杂了。connect不成功的话,socket还要重新生成。connect也是会阻塞的,因为connect在TCP协议层是会先发SYN包的,这期间是阻塞的(当然也可以设置成非阻塞模式,由epoll来监控),如果收到服务返回的ACK+SYN包,connect才会返回成功。如果不成功,协议层还会重试连接,这些都是应用层看不到的。所有这些都是因为网络是不可靠的,TCP是复杂的,所以网络IO随之也变得复杂起来。不像文件IO,可以像使用函数一样来使用,而网络IO必须处理失败或者超时的情况。

        目前来说主流的网络IO都是用的IO多路复用+非阻塞socket模式。注意,这也是同步的IO,异步的IO必须是一些特殊的API,我们主流的方式都是同步方式。IO多路复用就是select或epoll方式,更多的是epoll方式,我们在之前的文章有介绍过epoll(http://blog.csdn.net/coolmeme/article/details/9768847),这里不讨论细节。epoll的思想是不是阻塞于某个socket的读写,而是阻塞于一个新的系统调用,epoll_wait上,而这个系统调用的返回可以告诉我们有一个或者多个fd可读或可写,注意,epoll可以wait的不仅仅是socket的fd了,各种fd都可以被它来监控。由于epoll的高效,于是one loop per thead这个编程概念备提出来了,就是在一个线程上完成对成千上万设置上几十万的fd的活动来监控。然后由一组处理线程来实现业务操作(比如技术,数据库操作等)。这就是现代网络服务的基本模型,大同小异。

        网络IO的复杂性还在于数据是按流的方式来传输的,所以每次socket从内核提交给应用层的数据,你应用层不能立刻丢弃,因为可能不是你要的全部,你得保存下来,等到若干次read后,可能才是一个完整的包,所以一定要有应用层的buffer。这样就不能像udp那样单次无记忆的处理一个请求来的容易。

        网络IO的复杂性还有一点是来源于TCP协议的复杂性。比如三次握手,4次挥手,流量控制,NO_DELAY设置,TIME_WAIT状态(先发FIN包惹来的麻烦),其他众多的socket选项等,要全面理解可能才能写出比较能把控的网络IO程序来。

       目前文件IO和网络IO都是很成熟的技术了。通用高效的解决方案都有,我们自然也不必重新早轮子,找一些开源的项目就可以。但是我们若是理解了其中的原理和思想,对我们写应用层也是很有帮助的。

        

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值