在《系统编程-文件IO》中简单介绍了文件I/O的基本流程,无论选项或者参数多么变化多端,其流程大抵相同,不过是获取文件描述符,用描述符进行操作,关闭描述符,三步而已。那么文件读写又是怎样的流程?需要注意什么?
write/read
在说明这些常见出错之前,就必须先了解其基本用法了。需要注意的是,write/read是不带缓冲的,调用一次,写一次。与fwrite/fread有区别,另外write/read为系统调用,频繁地系统调用将会增加开销,可参考《库函数和系统调用的区别》。1
2
3
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
参数解释:
- fd 文件描述符,这个应该不用多做解释
- buf 要写入的内容,或者读出内容存储的buf,合适的大小非常关键
- count 读或写的内容大小
这里有两点需要注意一下。
返回值为ssize_t类型,因为它的返回值可以为负,表示出错,有趣的是这样一来使得其能表示的读写字节范围少了近一半。
返回大于0,表示读或写入对应的字节数。对于read,返回0表示到文件结尾。
另外,我们还注意到,write函数的第二个参数由const修饰。为什么要使用const来修饰?
很显然,在写的过程中,write函数不应该对buf的内容进行修改,它仅仅是从buf中读取罢了。这里在编码时常用的设计,如果不希望该函数修改其内容,则加上const限定符。const详细说明参考《const关键字到底该怎么用?》。
那么返回的读写大小,和参数里的count大小有何区别?前者是真实读写的字节数,而后者是期望读写的字节数。举个简单的例子,文件中有16字节内容,而你尝试读64字节,自然最终只会读到16字节。
正常读写
正常读写的例子如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40//来源:公众号【编程珠玑】
//博客:https://www.yanbinghu.com
//file.c
int main()
{
char writeBuf[] = "https://www.yanbinghu.com";
char readBuf[128] = {0};
/*可读可写,不存在时创建,有内容时截断*/
int fd = open("test.txt",O_RDWR | O_CREAT | O_TRUNC);
if(-1 == fd)
{
perror("open failed");
return -1;
}
/*写内容*/
ssize_t wLen = write(fd,writeBuf,sizeof(writeBuf));
if(wLen < 0)
{
perror("write failed");
close(fd);
return -1;
}
printf("write len:%ld\n",wLen);
ssize_t rLen = read(fd,readBuf,sizeof(readBuf));
if(rLen < 0)
{
perror("read failed");
close(fd);
return -1;
}
readBuf[sizeof(readBuf)-1] = 0;
printf("read content:%s\n",readBuf);
close(fd);
return 0;
}
编译运行,然后你就会惊喜地发现,结果并不是如你想地那样:1
2
3
4 gcc -o writeFile file.c
./writeFile
write len:26
read content:
我们查看文件可以看到内容已经写进去了,但是读取出来地内容却是空!
这是为何?
理解这个问题需要理解文件描述符和偏移量。
文件描述符
文件描述符虽然只是一个整型值,但它只是一个索引值,它指向了该进程打开文件的记录表。还记得常说的“一切皆文件”吗?实际上,即使你每打开一个TCP链接,都会有一个对应的文件描述符。这个记录表中包含了很多与文件相关地信息,例如文件偏移量,inode,状态标志等等。
而你每一次进行读写,都会影响所谓地文件偏移量。
因此你在第一次进行写之后,文件偏移量类似于下面这样:
那么你进行第一次读的时候,文件偏移已经到文件的末尾了(此时函数返回值为0),所以你肯定读不出任何内容,因此你需要移动偏移指针。
设置偏移量
为了读取写入后的内容,我们必须要设置偏移量,设置成像下面这样:
有人可能会好奇,这最后为什么还有一个\0?很显然,它被自动加上了,具体原因可以参考《NULL,0,’0’你真的分清了吗》。
还有人会问,你怎么看出有一个\0?用od命令看一下就知道了。1
2
3
4 od -c test.txt
0000000 h t t p s : / / w w w . y a n b
0000020 i n g h u . c o m \0
0000032
现在看到了吧。
为了设置偏移量,我们需要用到函数lseek:1
2
off_t lseek(int fd, off_t offset, int whence);
成功返回新的文件偏移量,出错返回-1。
有必要对参数进行解释
- offset 相对于whence的偏移量
- whence 相对位置
其中whence有三个值
- SEEK_SET 文件开始处
- SEEK_CUR 当前位置
- SEEK_END 文件末尾
举个例子,假设当前offset为-4,whence为SEEK_CUR,那么当写完内容,并设置该选项后的文件偏移位置如下:
https://www.yanbinghu. | com | |
---|---|---|
↑ |
注意,offset是可以为负的。
说白了可以设置偏移位置,而设置可以相对三个位置,开头,当前和结尾。
读取写入的内容
好了,为了读取到我们写入的内容,我们已经知道怎么做了,就是设置偏移量在文件开头,即在读之前加上下面的语句:1
lseek(fd, 0, SEEK_SET);//注意检查返回值
然后再次编译运行:1
2write len:26
read content:https://www.yanbinghu.com
如你所愿!
常见报错
使用不当或者出错的时候会有错误信息,这在编码的时候就需要注意检查。
Bad file descriptor
通常使用了一个并不合法的文件描述符,例如,该文件描述符已经关闭。通常你可以通过下面的命令来观察文件描述符的打开情况:1
$ ls -al /proc/`pidof procName`/fd/
这里的procName是你正在运行的程序名。
也有可能是你打开模式不对,例如,以只读方式打开,却尝试写。
Interrupted system call
通常是在读写过程中被中断,常见的如对socket进行读写时,链接被意外中断,或者读写时,进程被中断等等。
File exists
通常在你想创建一个文件,但是文件已经存在的情况。
No such file or directory
就如字面意思,通常是文件或者目录不存在,也许你使用了O_CREATE标志,但是如果你的目录不存在,文件也无法创建成功。
还有一种情况是,你已经打开了该文件,程序执行过程中,该文件又被人删除了,删除后又创建了一个文件名一样的文件,这样的情况下,也有可能会提示该错误。
Too many open fileswrite
进程打开的文件过多。一个进程打开的文件数量是有限的,具体可以通过:1
2$ ulimit -n
65535
至于当前已经打开了多少,可以这样统计:1
$ ls -l /proc/`pidof proName`/fd/ |wc -l
proName为你的进程名。
总结
一些常见错误中很多涉及到网络的读写,这里暂时没有提及。
一般情况,不会用同一个文件描述符对文件进行既读又写,一旦出现这样的场景时,需要注意偏移量的设置。虽然本文的I/O函数不带缓冲,但是读写时,选择合适的buf大小也非常关键。
另外编程中也有以下建议:
- 检查接口的返回值,处理出错场景
- 对于不期望被修改内容的参数,添加const限定符
- 善用man手册