谈谈系统级I/O
对于I/O每个程序员并不陌生,但从菜鸟入门阶再到进阶,每个阶段对I/O都会有新的认识见解,从最开始的printf
函数到Linux中的管道再到多线程使用中读写I/O,网络I/O的概念,每个地方都感觉有I/O的身影,但模模糊糊,含义好像都不太一样,困惑也渐渐增多…
1. Unix I/O
1.1 输入/输出
输入/输出(I/O)是在主存(DRAM)和外部设备(例如磁盘驱动器、终端、网络)之间复制数据的过程。输入操作是I/O设备复制数据到主存,而输出操作是从主存复制数据到I/O设备。
图稍微配的有点糙,但是能很清晰的说明输入上段文字中所描述的关系,复制数据的过程在I/O总线中进行。
在UNIX操作系统中,所有的外围设备(上图中显示的)都被看作是文件系统中的文件,一个Linux文件就是m个字节的序列:BO,B1,B2...
。因此,所有的输入/输出都要通过对应的读文件或写文件完成。这种将设备优雅地映射为文件的方法,允许Linux内核引出一个简单、低级的应用接口口,称为为Unix I/O
。也就是说,通过一个单一的接口就可以处理外围设备和程序之间的所有通信。
文本文件(比如txt文件)与文件(上述的文件含义)要区分开
主存(L3高速缓存)的速度远远大于本地磁盘(本地二级存储)和web服务器(远程二级存储),由于这种巨大的速度差,我们在多线程中总是提到I/O瓶颈,I/O时间观,例如从SSD读1M连续数据真实延迟是1毫秒,但对CPU的感觉来说是一个月的漫长时间!而这个等待时间我们不能让CPU白白浪费,要搞点别的事,然后就是I/O模型的各种进化版,最后到异步的操作了。
1.2 操作文件的方式
打开文件:因为大多数的输入/输出是通过键盘和显示器实现的,为了方便,UNIX做了特别的安排。一个应用程序通过要求内核打开相应的文件,内核将返回一个非负整数,称为描述符,记录打开文件的所有信息:标准输入(描述符0)、标准输出(描述符1)、标准错误(描述符2)。
改变当前文件位置:内核保持一个文件的位置k,初始为0,表示从文件开始处偏移的字节数。通过seek操作。
读写文件:读操作就是从文件拷贝n个字节到存储器,如果是从k处开始,就是拷贝k+n为止。文件的大小为m,如果k≥m就会触发(EOF),所有就不需要明确的EOF字符了。写操作就是从存储器拷贝n个字节到文件当前位置k处。
关闭文件:通知内核关闭文件。内核释放文件打开时创建的数据结构,将描述符恢复到可以的描述符池中。无论一个进程因为哪种原因终止,内核都会关闭所有打开的文件并释放他们的内存资源。
2. 标准I/O函数
有了上面的一些概念,我们可以看看C语言中一些常见的容易混淆的函数。各种I/O函数都存放在stdio.h
头文件中
2.1 格式化输出
1 |
|
printf(...)
函数其实是fprint
的带默认参数版,等价于fprint(stdout,...)
int printf(const char *format,...)
对比int fprintf(FILE *stream, const char *foramt,...)
因为所有外围设备(这里指显示器)都是文件,我们只要把后者函数的stream参数默认为标准输出stdout即可。所以我们也可以把这个stream参数用一个FILE *
参数来代替,那就直接格式化输出到文件里,这个我们后面会继续讲到。
流(stream)是与磁盘或者其他外围设备关联的数据的源或者目的地,UNIX中文本流和二进制流是相同的。打开一个流将把该流与一个文件或设备连接起来,关闭流将断开这种连接。打开一个文件将返回一个指向FILE类型对象的指针,该指针记录了控制该流的所有必要信息。我们可以将
文件指针
和流
等同
A very important concept in C is the stream. In C, the stream is a common, logical interface to the various devices that comprise the computer.In its most common form, a stream is a logical interface to a file. As C defines the term “file”, it can refer to a disk file, the screen, the keyboard, a port, a file on tape, and so on.Although files differ in form and capabilities, all streams are the same. The stream provides a consistent interface and to the programmer one hardware device will look much like another.
https://www.cquestions.com/2009/01/what-is-stream-in-c-programming.html
此外,还有个int sprintf(char *s, const char *foramt,...)
函数,与printf
函数基本相同,但其输出将被写入到字符串s中,具体可以看C语言sprintf()函数:将格式化的数据写入字符串。为什么会有这个函数,反正把内存中的数据写到文本,显示器等文件中,当然我也可以直接不变位置还是写到内存中的另一个地方,所有才有了这个char *s
。
2.2 格式化输入
1 |
|
格式化输入这边的原理跟输出,不展开
2.3 字符输入输出
1 |
|
这个的getchar
函数等同于getc(stdin)
,而相对的输出这边putchar
函数等同于putc(c,stdout)
。
2.4 文件操作
上面都是常见的标准输出到显示器,从键盘标准输入,这些其实都是文件操作函数,只是带有默认输入输出的函数。
那么,究其根源,处理与文件有关的通用函数来了!
1 |
|
FILE *fopen(const char *filename, const char *mode)
fopen
函数打开filename指定的文本文件,并返回一个与之相关联的流。如果打开操作失败,则返回NULL。访问模式mode就是常见的:r
,w
,a
等,含义就不一一说了,网上很多
相对应的,int fclose(FILE *stream)
函数讲所有未写入的数据写入流stream中,丢弃缓冲区中的所有未读输入数据,并释放自动分配的全部缓冲区,最后关闭流,若出错则返回EOF,否则返回0。
我们可以用fwrite
直接把数据输出到流stream中,相应的是fread
而fprintf
是对输出进行格式化再写到stream流中,对应的是fscanf
fputs
就是讲字符使出到流stream中,对应的是fgetc
顺便提一下int fflush(FILE *stream)
,对输出流来说,该函数将已写到缓冲区但尚未写入文件的所有数据写入到文件中。对输入流来说,其结果是未定义的。如果在写的过程中发生错误,则返回EOF,否则返回0。fflush(NULL)
将清洗所有的输出流。
刚开始学python也遇到缓冲区,print函数中flush参数的问题,以及缓冲策略。参考Python File Buffer和python的print(flush=True)实现动态loading……效果
3. UNIX I/O函数
上面说的标准I/O函数其实算是叫高级别的I/O函数,在Linux系统中,这些高级I/O函数都是通过使用由内核提供的系统级Unix I/O函数(较低级)来实现的。大多数时候,高级别I/O函数工作良好,没必要直接使用Unix I/O。那什么还要讲呢,两点,一是能帮助理解其他的系统概念,二是又是你除了Unix I/O函数以外别无选择。
1 |
|
进程是通过open
函数来打开一个已存在的文件或者创建一个新文件,返回一个描述符数字。返回的描述符总是进程中当前没有打开的最小描述符
1 |
|
应用程序是通过分别调用read
和write
函数来执行输入和输出的。
1 |
|
这里写了个例子,打开foo.txt
,从标准输入(即键盘输入)中获取字符只要read
函数读到的字节数不为0就把字符写进foo.txt
中。
4. 更健壮的RIO包
RIO包更强壮一点,带有无缓冲的输入输出函数以及带缓冲的输入函数,但这不是我这篇文章的重点,有兴趣可以去网上找找。
5. 我该使用哪些I/O函数
Unix I/O模型实在操作系统内核中实现的。应用程序可以通过诸如open
、close
、lseek
、read
、write
、stat
这样的函数来访问Unix I/O。较高级别的RIO和标准I/O函数都是基于Unix I/O函数来实现的。具体可以看第三节的那个图,一目了然
那么,选哪一个? 对我自己来说:只要有可能就使用标准I/O。对磁盘和终端设备来说,标准I/O函数是首选方法。大多数C程序员在其整个职业生涯只使用标准I/O!
6. I/O重定向
Linux shell提供了I/O重定向操作符,允许用户将磁盘文件和标准输入输出联系在一起。例如ls > foo.fxt
,使用shell加载和执行ls程序,将标准输出重定向到磁盘文件foo.txt
。一些更加复杂的重定向操作可以看Linux Shell 1>/dev/null 2>&1 含义
7. 总结
断断续续花了两天时间做了这篇总结,回头想想起因是啥,回头C程序设计语言
这本圣经时,以前只是看到结构体那一章就盖上书了,结果这次不仅翻到了后一章,还把附录翻了一遍,刚好手头有本CSAPP
,越翻又糊涂又明白,遂总结了一下。
其实写到后面,发现,有些低级函数作为一般程序员压根用不到,还是得用高级函数,但是在这学习的过程中,很多相关概念才是最让人解惑,让自己理解整个过程以及其他系统概念。其实平常常用的文本文件读写,Python相关的函数封装地更简单,直接两行代码搞定。然而,所有编程语言都基于操作系统,当总结了相关概念之后,无论啥语言, 关于I/O根本的东西都是一样的。而自己偏爱使用Python所谓高级语言后,简单便捷,但所有的东西经过封装之后隐藏了幕后的细节,只是把语法当成工具,或许,工具就够了,又或许,还不够。
当了解了系统I/O之后,接下去,回到过去,跟着历史和大佬们一起,开发Linux I/O模型!!
本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!