Linux笔记---系统文件I/O

1. open函数和close函数

#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 <unistd.h>
int close(int fd);

open函数是一个用于打开或创建文件的系统调用,C语言的fopen函数就是对该函数的封装。

  1. pathname:要打开或创建的文件的路径名(相对路径或绝对路径)。
  2. flags:控制文件打开模式的标志,如只读、只写、读写、创建等。
  3. mode:当创建新文件时,指定文件的权限。这个参数是可选的,仅当在flags中设置了O_CREAT标志时使用。

close函数对应的自然就是fclose函数,将open函数的返回值作为参数传入即可关闭文件,就不过多解释了,重要的是open函数的使用。 

1.1 flag参数

该参数是一个整型值,通过一系列的宏来传递参数,常用的有:

  • O_RDONLY:只读模式打开。
  • O_WRONLY:只写模式打开。
  • O_RDWR:读写模式打开。
  • O_CREAT:如果文件不存在,则创建它。使用这个选项时,需要提供第三个参数mode,指定新文件的权限。
  • O_EXCL:与O_CREAT一起使用时,如果文件已存在,则返回错误。这用于测试文件是否存在。
  • O_TRUNC:如果文件已存在且为普通文件且以只写/读写方式打开,则其长度被截断为0。
  • O_APPEND:写入时将数据追加到文件末尾。

flags本质上是一个位图,而这些宏本质上都是只有一个比特位为1的整型值。

这样的设计方式,使得我们可以利用flags同时传递多条信息,例如:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main()
{
    int fd = open("test.txt", O_CREAT | O_WRONLY | O_TRUNC);
    close(fd);
    return 0;
}

// open函数内部判定某个flag是否被选用的原理如下
if(flags & O_CREAT)
{
    // ...    
}
if(flags & O_WRONLY)
{
    // ...
}
if(flags & O_TRUNC)
// ...

 1.2 mode参数

mode参数指定新创建文件的权限(只有当flags参数中包含O_CREAT时使用),通常与umask的设置相结合来确定最终权限。

一些常见的权限包括:

  • S_IRUSRS_IWUSRS_IXUSR:分别表示文件所有者的读、写、执行权限。
  • S_IRGRPS_IWGRPS_IXGRP:分别表示用户组的读、写、执行权限。
  • S_IROTHS_IWOTHS_IXOTH:分别表示其他用户的读、写、执行权限。

传参的原理和方式与flags相同,但是使用上面的这些宏来传参太过麻烦了,这些宏的数值实际上是有规律的,分别与权限的八进制表示相对应。

不清楚权限的八进制表示方式的小伙伴可以参考我的这篇博文:Linux笔记---Linux权限理解_linxu用户权限的笔记-CSDN博客

例如,假设"test.txt"在当前目录下不存在,我们希望在创建该文件时将文件的读写权限全部放开:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main()
{
    int fd = open("test.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
    close(fd);
    // 等价于
    // FILE* fp = fopen("test.txt", "w");
    // fclose(fp);
    return 0;
}

open函数新建文件时,如果我们没有传入mode,那么新文件的权限就会显示为乱码:

1.3 文件描述符

open函数的返回值是对应文件的文件描述符,即当前程序中已打开的文件在管理文件指针的数组(file* fd_array[])中的下标,从0开始依次递增。

但由于程序在启动时会默认打开stdin、stdout、stderr,所以我们自己打开的文件的文件描述符是从3开始递增的。 

分配下标的规则也很简单:当有文件被打开时,依次遍历fd_array数组,找到第一个为空的下标。

假如我们将1号文件关闭,再另打开一个文件,那么这个文件就会占据1号下标,同时被各种库函数当作标准输出:

#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>

int main()
{
    close(1);
    int fd = open("test.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);

    if(fd == 1)
        printf("喜欢stdout的小朋友你们好啊!我是test.txt,stdout已经被我干掉了!\n");
    fprintf(stdout, "fd: %d\n", fd);

    return 0;
}

可以看到,printf函数将内容输出到了test.txt而不是标准输出。 


2. write函数和read函数

2.1 write函数

write函数用于向文件描述符中写入数据。

#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
  • fd:文件描述符,表示要写入的文件、管道、套接字等。
  • buf:指向要写入数据的缓冲区的指针。
  • count:要写入的数据的字节数。

write函数的返回值为实际写入的字节数,如果发生错误则返回 -1,并设置errno来指示错误原因。

2.2 read函数

read函数用于从文件描述符中读取数据。

#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
  • fd:文件描述符,代表了需要读取的文件或设备。
  • buf:一个指向用户分配的缓冲区的指针,read函数将把读取到的数据写入该缓冲区。
  • count:需要读取的字节数,表示最多读取count字节数据。

read函数的返回值为实际读取到的字节数,如果到达文件末尾则返回0,如果发生错误则返回 -1,并设置errno来指示错误原因。

2.3 示例代码

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>

int main() {
    int fd;
    char buffer[1024];

    // 打开文件
    fd = open("test.txt", O_RDWR | O_CREAT, 0666);
    if (fd == -1) {
        perror("open");
        return 1;
    }

    // 写入数据
    const char *data = "Hello, World!";
    ssize_t bytes_written = write(fd, data, strlen(data));
    if (bytes_written == -1) {
        perror("write");
        close(fd);
        return 1;
    }

    // 移动文件指针到文件开头
    lseek(fd, 0, SEEK_SET);

    // 读取数据
    ssize_t bytes_read = read(fd, buffer, sizeof(buffer));
    if (bytes_read == -1) {
        perror("read");
        close(fd);
        return 1;
    }

    // 输出读取的数据
    buffer[bytes_read] = '\0';
    printf("Read: %s\n", buffer);

    // 关闭文件
    close(fd);

    return 0;
}

首先使用open函数以读写方式打开一个文件,如果文件不存在则创建它。然后使用write函数向文件中写入数据,接着使用lseek函数将文件指针移动到文件开头,再使用read函数从文件中读取数据,最后关闭文件。

3. 重定向

我们在命令行当中可以使用">"、">>"、"<"来进行重定向,即将一个程序的结果输出到文件中,或者从一个文件中读取程序的输入。

重定向的类型

  • [>]输出重定向:将本来应该输出到标准输出(终端屏幕)的数据重定向到一个文件中。
  • [<]输入重定向:将本来应该从标准输入(键盘)读取的数据重定向到一个文件中。
  • [>>]追加重定向:将数据追加到一个文件的末尾,而不是覆盖文件的内容。

那么,重定向的本质是什么呢? 

还记得我们在文件描述符那里举得例子吗?这就是重定向的本质,即修改文件描述符所指向的内容,也是我们在代码层面进行重定向的方式之一。

但是,利用下标分配的机制来进行重定向未免有点繁琐,而且不够精确,可读性,可维护性都较差。

在代码层面,我们常用的重定向的方式是使用dup2系统调用。

3.1 dup2函数

int dup2(int oldfd, int newfd);

它的作用是将oldfd所指的文件描述符复制到newfd,并且返回newfd

如果newfd已经打开,则先关闭newfd,再进行复制。该函数成功时返回新的文件描述符,失败时返回 -1,并设置相应的错误码。

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main()
{
    int fd = open("test.txt", O_WRONLY | O_CREAT, S_IRUSR | S_IWUSR);
    dup2(fd, 1);
    printf("喜欢stdout的小朋友你们好啊!我是test.txt,stdout已经被我干掉了,这条消息不会写到标准输出啦!\n");
    return 0;
}

3.2 命令行重定向

实际上,我们平时写的输出重定向是简写的形式:

./a.out > normal.txt

完整的写法如下,即将标准输出重定向到log.txt:

./a.out 1 > normal.txt

并且支持多次重定向:

./a.out 1 > normal.txt 2 > error.txt

这也在一定程度上解释了stderr存在的原因,虽然stdout和sdterr都指向显示屏,但是我们可以使用重定向的方式,将错误信息与输出信息分开。

如果需要将stdout和stderr重定向到同一个文件,需要采用以下的写法:

./a.out 1 > log.txt 2 >> log.txt
./a.out 1 > log.txt 2 > &1

错误写法(每次重定向都会重新打开一次文件,导致stdout的内容被覆盖):
./a.out 1 > log.txt 2 > log.txt