查看原文
其他

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

守望先生 编程珠玑 2022-09-10

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

作者:守望先生

ID:shouwangxiansheng

我们都听过Linux下一切皆文件,实际上无论是普通的文件读写,还是网络IO读写,它们都有着类似的操作过程。本文通过基本文件IO操作,来了解Linux“一切文件”的读写。当然过程中穿插着很多其他内容。

文件I/O过程

在介绍具体的函数使用之前,我必须说明一下文件I/O的基本过程。它们类似过程如下:

  • 以某种模式打开文件,获取一个文件描述符

  • 对文件进行读写

  • 不需要时,关闭文件描述符

文件描述符是什么?你可以认为是一个对文件进行操作的凭据,你只有通过它才能对文件进行读写。它是一个非负整数。通常0是标准输入,1是标准输出,2是标准错误(参考《如何理解Linux shell中“2>&1”》)。正是有了它们,你的简单程序才可以从控制台读入数据,输出日志,输出错误打印等等。

记得很小的时候,家里连压水的工具都没有,需要用水的时候,都是用一个小点的桶从井里打水。

类比文件I/O操作,打开井盖,拿到绑着绳子的水桶,就像是打开文件,获取文件描述符;而打水的过程,就像对文件进行读写;最后需要的时候,又把桶放回去,并盖上井盖;而这就像关闭文件描述符。

当然了,如果嫌弃里面的小桶打水太慢,有的人可能会用一担大桶用来装水,装满一担后,再挑走使用。而这个过程就像使用了缓冲。(参考《不可不知的三种缓冲》)。

说了这么多废话,文件I/O到底怎么操作呢?本文介绍的是不带缓冲的I/O函数

打开文件,获取文件描述符

主要函数:

#include<fcntl.h>
int open(const char *pathname, int flags, mode_t mode);

参数解释:

  • pathname 文件名

  • flags 打开选项

这里的文件名应该不用过多解释,但是flags需要做一些说明,
它须指定以下五个中的一个:

  • O_RDONLY  只读

  • O_WRONLY  只写

  • O_RDWR    可读可写

  • O_EXEC   执行打开

  • O_SEARCH  搜索打开(针对目录)

而下面的选项是可选的:

  • O_APPEND 写时追加到文件末尾

  • O_CREAT  文件不存在时创建,且必须指定文件访问权限位

  • O_TRUNC    文件存在时,且以只写,或者读写方式打开,则截断长度为0

  • ……

当打开成功时返回文件描述符,否则返回-1,并且设置errno。

读写操作

读写操作主要有两个函数:

#include<unisdt.h>
ssize_t read(int fd, void *buf,size_t nbytes);
ssize_t write(int fd, const void *buf,size_t nbytes);

参数说明:

  • fd 文件描述符

  • buf 要读写的内容

  • nbytes 读写的内容大小

这里的fd就是前面拿到的文件描述符。篇幅有限, 本文暂不涉及具体的读写介绍。

关闭文件

调用close函数即可,它的参数是前面打开的时候获得的文件描述符

#include <unistd.h>
int close(int fd);

成功返回0,失败则返回-1,并且会设置errno。

实例

以上都太过理论化了,那么理论结合实际来看看,究竟是怎样的。

打开一个不存在的文件

这是最简单的情况,现在假设,当前目录下没有test.txt

//来源:公众号【编程珠玑】
#include<stdio.h>
#include<fcntl.h>
#include<unistd.h>
#include<string.h>
#include <errno.h>
int main(void)
{
    int fd = open("test.txt",O_WRONLY);
    if(-1 == fd)
    {
        //perror("open failed:");
        printf("open failed:%s\n", strerror(errno)); 
        return -1;
    }
    printf("open ok\n");
    char test[] = "wechat:shouwangxiansheng\n";
    ssize_t len = write(fd,test,sizeof(test));
    if(-1 == len)
    {
        perror("write failed");
    }
    close(fd);
}

运行报错:

open failed: No such file or directory 

还记得前面说的,如果出错就会设置errno吗?当open返回-1(很多系统接口类似)时,就会设置errno,这个时候就可以调用perror接口打印对应的错误信息。便于我们定位问题。即:

perror("open failed:");
printf("open failed:%s\n", strerror(errno)); 

上面两种方式都可以打印出错误信息,区别在于,前者输出到标准错误,后者输出到标准输出。

还记得在《不可不知的三种缓冲》中说的吗?标准错误通常是不带缓冲的

打开一个文件,不存在时创建

既然不存在时,会打开失败,那么不存在就创建好了,这就用到了O_CREATE标志。因此修改open函数那一行:

int fd = open("test.txt",O_WRONLY | O_CREAT);

运行结果:

open ok 

并且会在test.txt发现写入的内容。

注意到,多个标志使用|构成flags参数。

打开一个文件,存在时截断

好了,前面已经实现了文件不存在时,创建,存在时也可以正常打开,如果存在时,又不想要原先的内容?那就需要用到O_TRUNC标志。
修改open行如下:

int fd = open("test.txt",O_WRONLY | O_CREAT | O_TRUNC);

现在假设test.txt文件存在,且里面有内容,再次运行后,发现打开文件正常,且内容只有新加入的,而没有之前存在的。

在打开的文件后追加内容

如果想在打开的文件后追加内容,那么可以使用O_APPEND标志:

int fd = open("test.txt",O_WRONLY | O_CREAT | O_APPEND);

这样如果原来test.txt中有内容,则可以往文件中追加内容。

只读打开的文件进行写操作

前面提到了5个打开标志,如果以只读方式尝试写会怎样?
修改open行:

int fd = open("test.txt",O_RDONLY);

你会发现:

open ok
write failed: Bad file descriptor

以只读方式打开,却尝试写,自然是会写失败了。因此对应的操作要设置对应的标志位,否则会失败。

总结

以上就是文件I/O的基本操作。关键就三个步骤:

  • 以某种模式打开

  • 操作

  • 关闭

其中的模式无非是前面提到的一些模式,如读,写,或可读可写,而操作,常见为写入内容,读出内容等等,关闭就更好理解了。以上。

错误处理原则:
返回-1,则出错,会设置errno,可通过perror或者strerror打印错误信息。

相关精彩推荐

网络编程-一个简单的echo程序(0)

如何优雅地将printf的打印保存在文件中?

文件读写测试,磁盘读写测试全靠它

不可不知的三种缓冲类型


关注公众号【编程珠玑】,获取更多Linux/C/C++/数据结构与算法/计算机基础/工具等原创技术文章。后台免费获取经典电子书和视频资源

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存