Linux应用编程之文件IO
在Linux系统下,一般所有的设计都奉行一个原则,那就是一切皆是文件。无论你要访问一个硬件设备还是编写一个文档都需要操作相应目录下的文件,所以,对文件的操作可以说是Linux应用编程的基础,十分重要,本文会对Linux系统下与文件IO相关的库函数做一个介绍,并且分析文件在多进程中会被如何操作。
1. open函数与它的flags
在Linux编程中,如果要操作一份文件你必须得使用open函数打开相应的文件,打开成功后open函数会返回一个文件描述符fd,之后只需要操作fd便可操作对应的文件。那什么是文件描述符呢?在Linux系统中,文件本身是存放在硬盘空间中的,同时在硬盘空间中会有一个inode节点,该节点负责记录文件在硬盘中的存放信息(文件在硬盘中是分了好多块存放的,这样有利于文件的删除和添加内容)。但由于读写文件次数一般比较频繁,所以直接访问硬盘不仅效率低而且还会降低其的使用寿命,解决办法是访问文件时,还需要将访问对应文件的inode节点将分散在硬盘中的内容全部加载到内存中来,并且同样的为内存中的这份文件设置了一个节点(vnode)。啰嗦半天,主角登场了,由于上述原因所以现在访问文件只需要访问vnode即可找到对应内存中的文件,修改完成后将内存中的文件同步到硬盘中即可,但新的问题出现了,由于vnode节点信息较多,直接操作很有可能会破坏vnode中的值以至于造成无法修复的错误。这时,Linux系统为文件的操作封装了一系列的函数,并且在open函数返回了一个文件描述符(file descriptor),该文件描述符实质是一个整数,作为进程PCB中文件描述表指针的序号,对应指针指向包含了vnode与inode以及其他文件信息的结构体,之后所有的文件IO库函数操作的都将是指定的fd,通过下图可以更好的理解这些内容:
除此之外,由于所有的进程都是继承的init父进程,所以事先就已经打开了三个文件(标准输入,标准输出,标准错误),所以,open函数一般默认返回值从3开始。下面是open函数的声明,通过man 2 open
可以查看更详细的介绍:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
最开始的三个include包含了open函数用到的头文件,同时open函数接受两个或三个参数,返回一个int类型的值,这个值就是fd。open函数的第一个参数是文件路径,也就是要打开的文件存放的位置,第二个表示以何种方式打开,比如只读或只写,这些flag都以宏定义的方式设置好了,所以我们只需要用对应的宏即可,以下给出了常用的几种flag参数的宏:
flag | 功能 |
---|---|
O_WRONLY | 文件以只写的方式打开,若读文件将会失败 |
O_RDONLY | 文件以只读的方式打开,若写文件将会失败 |
O_RDWR | 文件以读/写方式打开,即能读也能写 |
O_APPEND | 文件以追加的方式打开,指针在文件末尾 |
O_TRUNC | 文件打开后会清空文件中所有内容 |
O_CREAT | 若要打开的文件不存在,open函数将创建一个要打开的文件,文件的权限由mode参数指定,如果设置mode为0600,创建的文件权限为-rw-------,另外需要注意的是文件最后权限值等于(~umask & mode)。 |
O_EXCL | 与O_CREAT标志位一起使用,当文件存在时打开文件会返回-1值提示打开文件失败 |
O_NONBLOCK | 非阻塞式标志位,只用于打开设备文件,默认为阻塞式 |
O_SYNC | 同步标志位,添加该标志为可以保证每次write完磁盘空间与内存空间的值相同 |
另外,open函数支持多flag共同传参,例如下面这段程序:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main(){
int fd = -1; //file descriptor
fd = open("./test.txt", O_RDWR | O_CREAT | O_EXCL, 0755);
if(fd == -1){
printf("File is not opened\n");
return -1;
}
printf("File is successfully opened, fd=%d\n", fd);
_exit(0); //关闭全部文件描述符,退出程序
}
在这个例子中,文件以O_RDWR | O_CREAT | O_EXCL方式打开,如果文件存在程序提示错误结束程序,若不存在则在当前目录下创建test.txt文件,文件权限为(-rwxr-xr-x),并且可读可写。
2. close函数
与open函数相对应的便是close函数,该函数定义如下,其参数只有一个,表示要关闭的文件描述符,一般情况下一个open函数必须对应的有一个close函数用于关闭open函数打开的文件描述符。但有些时候,我们也会用_exit函数,这个函数可以关闭全部该程序打开的文件描述符,十分方便。
#include <unistd.h>
int close(int fd);
3. read函数
read函数用于读取open函数打开的某个文件描述符所选择的文件中的内容,其定义如下:
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
该函数有三个参数,fd表示要操作的文件,buf表示读取后内容存放的位置,count表示读取的字节数,最后函数会返回成功读取的字节数。以最开始的例子为例,我们在test.txt文本中输入一段内容“I like Linux!”,然后我们可以在程序中这么写:
ret = read(fd, buf, 13);
printf("Read %d bytes, File's content is /"%s/".", ret, buf);
这样,程序运行完后屏幕会打印Read 13 bytes, File’s content is “I like Linux!".
4. write函数
write函数类似于read函数,只不过它是负责将缓冲区内容写到文件描述符对应的文件中去,其定义如下:
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
该函数有三个参数,fd表示要操作的文件,buf表示即将写入文件缓存区的位置,count表示写入的字节数,最后函数会返回成功写入的字节数。还是以最开始的例子为例,我们清空test.txt文本中的内容,然后我们在程序中添加这样一段代码:
char buf[] = "I like linux too.";
ret = write(fd, buf, strlen(buf));
printf("Write %d bytes.", ret);
程序运行结束,可以在test.txt文件中发现增添新内容”I like linux too.“。
5. lseek函数
前面提到到write和read函数操作都是从当前文件偏移量开始操作,比如以O_RDWR方式打开文件,其偏移量为0,所以这时如果直接调用write函数就会覆盖最开始的内容,所以通过修改偏移量即可达到修改或读取任意位置的文件内容。那如何修改偏移量呢?这时就需要用到lseek函数,其定义如下:
#include <sys/types.h>
#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);
该函数同样有三个参数,fd表示要修改偏移量的文件的文件描述符,offset表示要偏移的量,whence表示从那开始偏移,最后函数返回成功偏移后的偏移量。对于whence这个参数,Linux系统给出了三种参考值:
宏 | 含义 |
---|---|
SEEK_SET | 从文件开始处偏移 |
SEEK_CUR | 从文件当前偏移量位置继续偏移 |
SEEK_END | 从文件末开始偏移 |
例如,我们要从文件末开始添加内容,在程序中可以添加如下内容:
ret = lseek(fd, 0, SEEK_END);
ret = write(fd, buf, strlen(buf));
printf("Write %d bytes.", ret);
lseek函数为文件操作提供了很强的灵活性,通过这个函数,我们可以制作一个空洞文件,也可以计算文件大小,作用十分巨大。
/*
* calculate file size
*/
fd = open("./1.txt", O_RDWR);
ret = lseek(fd, 0, SEEK_END);
printf("File size: %d bytes.", ret);
close(fd);
/*
* make cave file
*/
fd = open("./1.txt", O_RDWR); //1.txt是空文件
ret = lseek(fd, 10, SEEK_SET);
ret = write(fd, buf, strlen(buf));//buf = "ABC"
close(fd);
//最后文件内容为【10个字节的乱码+ABC】
空洞文件对于多线程文件操作用处非常大。
6. perror函数与erron变量
在Linux的文件操作中,每个函数都会有一个返回值,这个返回值通常情况下都是大于等于0的,如果返回值为-1说明操作失败,但究竟是什么原因导致的操作失败我们无从得知,毕竟返回值都是-1。争对这种情况,Linux系统中维护了一个变量erron,该变量用于记录导致操作失败的原因序号,我们通过这个序号可以找到对应的原因。除此之外,Linux系统还设置了一个函数perror,该函数进一步简化了打印失败的原因,它会自动寻找erron对应的原因,并且打印出来,其定义如下:
#include <stdio.h>
void perror(const char *s);
该函数接受一个传参,内容最后会打印到原因字符串前,用作提示字符,按照最开始的那个例子,我们将第一个printf函数替换如下:
perror("open");
如果文件存在,程序最后会打印“open: File exists“的字样。perror函数在调试函数是作用非常大,所以在操作文件失败后,我们应尽量多的使用该函数打印错误信息。
7. 文件共享与dup函数
Linux系统支持文件多次被同一进程打开或者被多个进程同时打开,这就相当与提供了文件共享的机制。文件共享的方法一般有三种:
- 在同一函数(进程)中多次打开同一文件,系统会分配给其不同的文件描述符,操作的文件都是内存中的那一份。
- 在不同进程中多次打开同一文件,系统按照各自进程中的文件描述符分配情况分配,可能出现文件描述符fd值相同的情况,并且操作的文件也都是内存中的一份。
- Linux系统提供了dup函数,该函数用于复制指定的文件描述符,并且返回一个新的文件描述符值。
下面有三个图片,分别对应上述三种实现机制:
第一种是单进程多次打开同一文件时文件描述符的情况,可以看到,打开的文件描述符分别指向不同的文件结构体,所以,如果我们交替使用两个文件描述符写入字符时,就会出现覆盖现象。比如我们用fd3向1.txt文件写入”aa“,然后用fd4向其写入"bb",这样交替3次最后我们可以在1.txt文件中发现这样的字样”bbbbbb“,这是为什么呢?原因就在于fd3与fd4分别指向不同的文件结构体,其各自维护了自己的偏移量,所以在交替写时就会出现覆盖现象。
第二种是多进程打开同一份文件,其实质与上一种情况大致相同,唯一一点不同的是每个进程中的fd值可能出现相同的情况。分析过后,第一种与第二种机制都会出现文件内容被覆盖的情况,如何避免呢?这时候就需要用到O_APPEND属性了,我们在每个进程种打开文件时都附加一个O_APPEND属性,当有一个进程或函数操作文件时,其会相应的移动其他文件描述符所指向文件结构体中的偏移量,这样就保证了文件操作的同步性。
第三种与前两种差异略大,共享机制是借助dup函数实现的。dup函数返回的文件描述符其实指向的就是原先被复制的文件描述符所指向的文件结构体,所以在操作fd3或者fd4实际上都是一样的。
dup返回的是一个系统能找到最小可分配值,不是我们设置的,这样有点不灵活,所以Linux还提供了一个改进版的函数dup2,该函数可以指定返回的fd值,它们的定义分别如下:
#include <unistd.h>
int dup(int oldfd);
int dup2(int oldfd, int newfd);
oldfd表示要被复制的文件描述符,newfd表示指定的文件描述符值,它们返回的都是成功获取的fd值。
8. fcntl函数
fcntl函数是用与操作文件描述符的函数,通过传递不同的命令参数实现不同的关于文件描述符的操作,其定义如下:
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );
Linux系统为该函数同样定义了很多宏,每个宏表示不同的CMD,这儿我们举其中一个例子,假设我们要复制一个文件描述符,我们可以编写下面的程序:
fd1 = fcntl(fd, F_DUPFD, 3);
这句代码的意思是复制fd文件描述符,分配一个大于等于3没有被占用的fd值给fd1,功能与dup函数有点类似。其他的功能详见man手册。
9. 文件重定位
文件重定位的意思是改变文件输出位置,比如我们的标准IO函数printf一般会将内容输出到屏幕上,如果我们像让其输出到其他文件中时,这时候就需要用到重定位,这是一个比较通俗的解释。正如这个例子,我们下面探讨以下实现的过程:
首先,一个文件在打开时其fd值都是大于等于3的,那0-2的数值究竟去哪里了,其实它们在创建进程时就已经分配给标准输入、标准输出、标准错误了,如果我们不想在屏幕上输出只需要关闭标准输出文件就好了。
其次,我们为了能够看到输出内容还需要指定一个具体的输出文件位置,这个输出文件的文件描述符必须是1,因为printf函数默认输出文件的fd值为1。
最后,我们按照这个思路编写如下函数:
fd1 = open("1.txt", O_RDWR);
close(1);
fd2 = dup(fd1);//dup函数默认分配最小可分配的文件描述符,并且其指向fd1所指向的文件表 fd2 = 1
printf("This is test!");
close(fd1);
这样,我们最后可以在1.txt文件中找到“This is test!”,这就是文件重定位的一个例子。另外,还需要注意的是最后必须用close函数关闭文件描述符,而不能用_exit函数,否则重定向会失败,我估计是与printf函数缓冲区的问题。
10. 标准文件IO库
以上全部内容都是在linux环境下实现的内容,如果放到windows系统下就会出错,因为不同系统间的API是不一样的,所以C语言专门在<stdio.h>函数库中集成了通用的文件IO函数,比如fopen,fwrite,fread,fclose,fseek,ffulsh,这些函数为跨平台文件操作提供了极大的方便。
以上内容全部自己手敲的,内容难免有所不足,还望见谅 ^ _ ^ 。