系统编程-文件读写

在《系统编程-文件IO》中简单介绍了文件I/O的基本流程,无论选项或者参数多么变化多端,其流程大抵相同,不过是获取文件描述符,用描述符进行操作,关闭描述符,三步而已。那么文件读写又是怎样的流程?需要注意什么?

write/read

在说明这些常见出错之前,就必须先了解其基本用法了。需要注意的是,write/read是不带缓冲的,调用一次,写一次。与fwrite/fread有区别,另外write/read为系统调用,频繁地系统调用将会增加开销,可参考《库函数和系统调用的区别》。

1
2
3
#include <unistd.h>
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
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
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
#include <unistd.h>
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,那么当写完内容,并设置该选项后的文件偏移位置如下:

注意,offset是可以为负的。

说白了可以设置偏移位置,而设置可以相对三个位置,开头,当前和结尾。

读取写入的内容

好了,为了读取到我们写入的内容,我们已经知道怎么做了,就是设置偏移量在文件开头,即在读之前加上下面的语句:

1
lseek(fd, 0, SEEK_SET);//注意检查返回值

然后再次编译运行:

1
2
write 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手册
守望 wechat
关注公众号[编程珠玑]获取更多原创技术文章
出入相友,守望相助!