专栏首页编程珠玑系统编程-文件读写这件小事

系统编程-文件读写这件小事

来源:公众号【编程珠玑】

作者:守望先生

ID:shouwangxiansheng

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

write/read

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

#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字节。

正常读写

正常读写的例子如下:

//来源:公众号【编程珠玑】
//博客: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;
}

编译运行,然后你就会惊喜地发现,结果并不是如你想地那样:

$ gcc -o writeFile file.c
$ ./writeFile
write len:26
read content:

我们查看文件可以看到内容可能已经写进去了,但是读取出来地内容却是空!

这是为何? 理解这个问题需要理解文件描述符和偏移量。

文件描述符

文件描述符虽然只是一个整型值,但它只是一个索引值,它指向了该进程打开文件的记录表。还记得常说的“一切皆文件”吗?实际上,即使你打开一个TCP链接,都会有一个对应的文件描述符。这个记录表中包含了很多与文件相关地信息,例如文件偏移量,inode,状态标志等等。

而你每一次进行读写,都会影响所谓地文件偏移量。

因此你在第一次进行写之后,文件偏移量类似于下面这样:

那么你进行第一次读的时候,文件偏移已经到文件的末尾了(此时函数返回值为0),所以你肯定读不出任何内容,因此你需要移动偏移指针。

设置偏移量

为了读取写入后的内容,我们必须要设置偏移量,设置成像下面这样:

有人可能会好奇,这最后为什么还有一个\0?很显然,它被自动加上了,具体原因可以参考《NULL,0,'0'你真的分清了吗》。

还有人会问,你怎么看出有一个\0?用od命令看一下就知道了。

$ 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:

#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是可以为负的。

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

读取写入的内容

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

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

然后再次编译运行:

write len:26
read content:https://www.yanbinghu.com

如你所愿!

常见报错

使用不当或者出错的时候会有错误信息,这在编码的时候就需要注意检查。

Bad file descriptor

通常使用了一个并不合法的文件描述符,例如,该文件描述符已经关闭。通常你可以通过下面的命令来观察文件描述符的打开情况:

$ ls -al /proc/`pidof procName`/fd/

这里的procName是你正在运行的程序名。

也有可能是你打开模式不对,例如,以只读方式打开,却尝试写。

Interrupted system call

通常是在读写过程中被中断,常见的如对socket进行读写时,链接被意外中断,或者读写时,进程被中断等等。

File exists

通常在你想创建一个文件,但是文件已经存在的情况。

No such file or directory

就如字面意思,通常是文件或者目录不存在,也许你使用了O_CREATE标志,但是如果你的目录不存在,文件也无法创建成功。

还有一种情况是,你已经打开了该文件,程序执行过程中,该文件又被人删除了,删除后又创建了一个文件名一样的文件,这样的情况下,也有可能会提示该错误。

Too many open fileswrite

进程打开的文件过多。一个进程打开的文件数量是有限的,具体可以通过:

$ ulimit -n
65535

至于当前已经打开了多少,可以这样统计:

$ ls -l /proc/`pidof proName`/fd/ |wc -l

proName为你的进程名。

总结

一些常见错误中很多涉及到网络的读写,这里暂时没有提及。

一般情况,不会用同一个文件描述符对文件进行既读又写,一旦出现这样的场景时,需要注意偏移量的设置。虽然本文的I/O函数不带缓冲,但是读写时,选择合适的buf大小也非常关键。

另外编程中也有以下建议:

  • 检查接口的返回值,处理出错场景
  • 对于不期望被修改内容的参数,添加const限定符
  • 善用man手册

本文分享自微信公众号 - 编程珠玑(shouwangxiansheng)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-12-20

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 系统编程-简洁而不简单的文件操作

    我们都听过Linux下一切皆文件,实际上无论是普通的文件读写,还是网络IO读写,它们都有着类似的操作过程。本文通过基本文件IO操作,来了解Linux“一切文件”...

    编程珠玑
  • 换个角度说Makefile

    作为Linux下的C/C++开发者,没接触过makefile一定说不过去,通常构建大型的C/C++项目都离不开makefile,也许你使用的是cmake或者其他...

    编程珠玑
  • 让你的专属博客更加漂亮

    hexo+github搭建专属个人博客的教程很多,这里也就不再介绍了,当然你也可以点击阅读原文查看。但是在初始搭建完成后,需要进行很多配置,使得网站功能更加健全...

    编程珠玑
  • Linux文件及文件I/O

    版权声明:本文为博主原创文章,转载请注明博客地址: https://blog.csdn.ne...

    zy010101
  • Struts2第三天:Struts2的值栈和OGNL表达式

    OGNL是Object-GraphNavigation Language的缩写,它是一种功能强大的表达式语言,通过它简单一致的表达式语法,可以存取对象的任意属性...

    AlbertYang
  • Android系统启动——7附录1:Android属性系统

    属性系统在Android 系统中大量使用,用来保存系统级别的设置或者在进程间传递一些简单的信息。每个属性由属性名称和属性值组成,名称通常是一串‘.’分割的字符串...

    隔壁老李头
  • 系统编程-简洁而不简单的文件操作

    我们都听过Linux下一切皆文件,实际上无论是普通的文件读写,还是网络IO读写,它们都有着类似的操作过程。本文通过基本文件IO操作,来了解Linux“一切文件”...

    编程珠玑
  • 文件操作——查找遍历

    前面说过了文件的读取、写入,那么在操作文件之前,一般我们需要找到操作的文件的路径,VBA对象库中,有一个FileSystem的模块,里面有关于文件操作的一些函数...

    xyj
  • 零基础学习 Python 之文件(一)

    大家好,这里是零基础学习 Python 系列,在这里我将从最基本的Python 写起,然后再慢慢涉及到高阶以及具体应用方面。我是完全自学的 Python,所以很...

    Rocky0429
  • 【论文分享】ACL 2020 细粒度情感分析方法

    情感分析是文本分类的一种,主要方法是提取文本的表示特征,并基于这些特征进行分类。情感分析根据研究对象的粒度不同可分为文本级、句子级、对象级等,分别对相应单位的文...

    zenRRan

扫码关注云+社区

领取腾讯云代金券