[Linux]基础IO

基础IO

  1. 空文件也要在磁盘占据空间。
  2. 文件 = 内容 + 属性。
  3. 对文件操作 = 对内容 + 对属性的操作,或是对内容和属性的操作。
  4. 标定一个文件,必须使用:文件路径 + 文件名。(可以表示唯一性)
  5. 如果没有指明对应的文件路径,默认是在当前路径进行文件的访问。
  6. 当我们把fopenfclosefreadfwrite等接口写完,编译形成二进制可执行文件之后,但是没有运行,文件对应的操作照样不会被执行,只有当程序被加载到内存变成进程后,才会执行文件对应的操作。
  7. 磁盘中有无数的文件,并不是所有的文件都被打开,当一个文件要被访问,前提是它得必须被打开。由用户进程调用对应的接口,操作系统执行打开的操作。

所以要研究文件操作的本质,就要研究进程和被打开文件的关系。

市面上有多种多样的语言,每种语言的操作文件的接口都不一样。但是文件是在磁盘的,磁盘是硬件,硬件由操作系统来管理,因此任何人想要访问磁盘不能绕过操作系统,必须使用操作系统提供的文件级别的系统调用接口,而操作系统只有一个,所以上层语言无论如何变化,它们对文件操作的接口无非就是对系统提供的接口的封装。

标志位传参

当我们在调用某函数的时候,可能会通过传递一个整数来表示某种状态,当参数比较少的时候还比较好说,当参数过多时,就不得不考虑其他的方法了。比如使用比特位的方式,一个整型有32个比特位,如果使用不重复的比特位表示的话,它可以表示32种状态。

示例

#include <stdio.h>

#define ONE (1<<0)
#define TWO (1<<1)
#define THREE (1<<2)
#define FOUR (1<<3)
#define FIVE (1<<4)

void print(int flags)
{
    if (flags & ONE) printf("one\n");
    if (flags & TWO) printf("two\n");
    if (flags & THREE) printf("three\n");
    if (flags & FOUR) printf("four\n");
    if (flags & FIVE) printf("five\n");
}

int main()
{
    print(ONE);
    printf("--------------------------\n");
    print(ONE | TWO);
    printf("--------------------------\n");
    print(ONE | TWO | THREE);
    printf("--------------------------\n");
    print(ONE | TWO | THREE | FOUR);
    printf("--------------------------\n");

    return 0;
}

系统调用接口

open

用于打开或创建文件。

  1. 函数原型:

    int open(const char *pathname, int flags)

    int open(const char *pathname, int flags, mode_t mode)

  2. flags参数对应的宏

    • O_RDONLY:以只读方式打开文件。

    • O_WRONLY:以只写方式打开文件。

    • O_RDWR:以读写方式打开文件。

    • O_CREAT:如果文件不存在,则创建文件。通常与O_WRONLYO_RDWR结合使用。

    • O_EXCL:与O_CREAT一起使用,用于确保创建的文件是最新的。否则open()函数会返回错误。

    • O_APPEND:以追加方式打开文件。

    • O_TRUNC:如果文件存在,并且以可写方式打开,那么会截断文件(即将文件大小设置为0)。

      示例

      #include <stdio.h>
      #include <sys/types.h>
      #include <sys/stat.h>
      #include <fcntl.h>
      
      #define FILE_NAME "test.txt"
      
      
      int main()
      {
          int fd = open(FILE_NAME, O_WRONLY | O_CREAT | O_EXCL);
          if (fd == -1)
          {
              perror("open");
              return 1;
      	}
      	close(fd);
      }
      
      

      如果使用上面的代码创建文件,会发现创建出来的文件的权限是乱的。那是因为在创建新文件的时候,必须要指定权限。

      只需要将int fd = open(FILE_NAME, O_WRONLY | O_CREAT | O_EXCL);改为int fd = open(FILE_NAME, O_WRONLY | O_CREAT | O_EXCL, 0666);

write

用于将数据写入指定的文件描述符。

  • 函数原型:ssize_t write(int fd, const void *buf, size_t count);

    示例

    int main()
    {
        int fd = open(FILE_NAME, O_WRONLY | O_CREAT, 0666);
        if (fd == -1)
        {
            perror("open");
            return 1;
        }
    
        int cnt = 5;
        char buffer[64];
        while (cnt)
        {
            sprintf(buffer, "%s:%d\n", "hellworld", cnt--);
            write(fd, buffer, strlen(buffer));
        }
        close(fd);
    
    }
    
    

read

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

  • 函数原型:ssize_t read(int fd, void *buf, size_t count);

文件描述符

上面总是提到文件描述符,那么到底什么是文件描述符呢?

我们知道一个进程可以打开多个文件,那么系统中一定会存在大量的被打开的文件,那么操作系统肯定要将这些被打开的文件管理起来。如何管理?先描述,后组织。即先为文件创建对应的内核数据结构(struct file {};),然后将这些对应的数据结构通过某种数据结构联系起来。struct file {};中包含了文件的大部分属性。

Linux下的struct file结构体。

我们在来看看文件描述符。

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

#define FILE_NAME(num) "test.txt"#num

int main()
{
    int fd1 = open(FILE_NAME(1), O_WRONLY | O_CREAT | O_APPEND, 0666);
    int fd2 = open(FILE_NAME(2), O_WRONLY | O_CREAT | O_APPEND, 0666);
    int fd3 = open(FILE_NAME(3), O_WRONLY | O_CREAT | O_APPEND, 0666);
    int fd4 = open(FILE_NAME(4), O_WRONLY | O_CREAT | O_APPEND, 0666);
    int fd5 = open(FILE_NAME(5), O_WRONLY | O_CREAT | O_APPEND, 0666);

    printf("fd: %d\n", fd1);
    printf("fd: %d\n", fd2);
    printf("fd: %d\n", fd3);
    printf("fd: %d\n", fd4);
    printf("fd: %d\n", fd5);

    close(fd1);
    close(fd2);
    close(fd3);
    close(fd4);
    close(fd5);
	
    return 0;
}

到这里,我们会发现两个问题,一是文件描述符为什么是从3开始,二是这个连续的整数是什么意思?

  1. 为什么是从3开始。

    当我们运行一个程序的时候,操作系统已经将3个文件给我们打开了。它们分别是(以C语言为例):标准输入(stdin),标准输出(stdout),标准错误(stderr)。分别对应着键盘,显示器,显示器。

    在上面代码的基础上加上如下代码。

    printf("stdin->fd: %d\n", stdin->_fileno);
    printf("stdout->fd: %d\n", stdout->_fileno);
    printf("stderr->fd: %d\n", stderr->_fileno);
    

    此时前三个文件描述符就出来了。

  2. 为什么是连续的整数

    当一个文件处于被打开的状态时,由于操作系统要管理,所以在内存中一定会为文件创建一个对应的struct file{}结构体。在进程的task_struct中会有一个struct files_struct *files的一个指针,这个指针指向struct files_struct,而它里面又包含一个struct file* fd_array[]的指针数组,这个指针数组保存的就指向被打开文件的指针。所以文件描述符就是对应着数组的下标。

重定向

stdout(显示器)默认对应的文件描述符是1,既然它是文件,就可以被关闭。关闭之后会出现什么结果呢?

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

#define FILE_NAME "test.txt"
int main()
{
    close(1);
    int fd = open(FILE_NAME, O_WRONLY | O_CREAT | O_TRUNC, 0666);
    if (fd < 0)
    {
        perror("open");
        return 1;
    }

    printf("open fd: %d\n", fd);
    fflush(stdout);
    close(fd);
}

从结果可以看到,当关闭了1号文件描述符后,在运行程序时,并没有对应的内容显示,这当然是正常的。但是当我们查看test.txt文件里面的内容的时候,却发现这个文件对应的文件描述符变成1号了。这是因为文件描述符的分配规则是,按照从小到大,依次寻找最小的且没有被占用fd。由于1号文件本来是对应的标准输出,但是此时你将标准输出关了,此时test.txt就被分到了一号文件描述符。那么输出时当然就是输向test.txt。这也就是重定向。

系统提供的接口

dup2()函数是操作系统为我们提供的重定向的接口。

函数原型:int dup2(int oldfd, int newfd);

参数:

  • oldfd:是要被复制的原始文件描述符。它必须是一个有效的、已经打开的文件描述符。
  • newfd:是目标文件描述符编号。dup2()会尝试将oldfd的内容复制到newfd所指定的文件描述符位置。
int main()
{
    //close(1);
    int fd = open(FILE_NAME, O_WRONLY | O_CREAT | O_TRUNC, 0666);
    if (fd < 0)
    {
        perror("open");
        return 1;
    }
	dup(fd, 1);
    printf("open fd: %d\n", fd);
    fflush(stdout);
    close(fd);
}

将之前的代码改写为这样,可以实现同样的效果。

posted @ 2024-11-26 11:55  羡鱼OvO  阅读(26)  评论(0)    收藏  举报