Linux 基础IO

时间:2024-10-23 08:38:19

1.C语言文件操作函数fopen

文件写入

#include <stdio.h>
#include <string.h>
int main()
{
    FILE* fp = fopen("myfile", "w");
    if (!fp) {
        printf("fopen error!\n");
    }
    const char* msg = "hello bit!\n";
    int count = 5;
    while (count--) {
        fwrite(msg, strlen(msg), 1, fp);
    }
    fclose(fp);
    return 0;
}

fopen

fwirte

fclose

打印内容到显示器的方式:
1.printf("%s");

2.fprintf(stdout,"%s");

3.puts("%s")输出字符串,并自动在字符串末尾添加换行符.

4.fputs("%s",stdout)字符串输出到指定的文件流,不会自动添加换行符。

5.fwrite("%s", 一个元素大小, 个数, stdout);

文件读

#include <stdio.h>
#include <string.h>
int main()
{
    FILE* fp = fopen("myfile", "r");
    if (!fp) {
        printf("fopen error!\n");
    }
    char buf[1024];
    const char* msg = "hello bit!\n";
    while (1) {
        //注意返回值和参数,此处有坑,仔细查看man手册关于该函数的说明
        ssize_t s = fread(buf, 1, strlen(msg), fp);
        if (s > 0) {
            buf[s] = 0;
            printf("%s", buf);
        }
        if (feof(fp)) {
            break;
        }
    }
    fclose(fp);
    return 0;
}

fread

2.系统文件io操作函数open

#include <unistd.h>
#include <string.h>
int main()
{
    umask(0);
    int fd = open("myfile", O_WRONLY | O_CREAT, 0644);
    if (fd < 0) {
        perror("open");
        return 1;
    }
    int count = 5;
    const char* msg = "hello bit!\n";
    int len = strlen(msg);
    while (count--) {
        write(fd, msg, len);
    }
    close(fd);
    return 0;
}

open

#include <unistd.h>   // 包含 write() 和 close() 函数
#include <fcntl.h>    // 包含 open() 和 O_* 常量
#include <stdio.h>    // 包含 perror() 和 printf()
#include <sys/types.h> //头文件用于定义一些数据类型和宏 pid_t size_t
#include <fcntl.h>
#include <unistd.h>

int open(const char *pathname, int flags, mode_t mode);
  • const char *pathname: 要打开的文件的路径名。
  • int flags: 文件打开的标志,可以是以下值的组合:
    • O_RDONLY: 以只读方式打开文件。
    • O_WRONLY: 以只写方式打开文件。
    • O_RDWR: 以读写方式打开文件。
    • O_CREAT: 如果文件不存在,则创建该文件。需要提供第三个参数 mode
    • O_TRUNC: 如果文件已存在并且以写入模式打开,并清空里面内容。
    • O_APPEND: 在文件末尾写入数据。
  • mode_t mode: 设置文件的权限.eg 0764 拥有者7wrx 所属组6wr 其他人4r。但实际权限还和unmask(权限掩码)有关。eg.open("test.c",O_CREAT,0666) unmask 0222 结果0666-0222=0444

返回值:

成功时,返回新的文件描述符(非负整数)。fd 

失败时,返回 -1,并设置 errno 以指示错误类型。

close

wirte

read

3.文件描述符fd

open打开/创建一个文件时,它的返回值就是文件描述符,本质就是一个int变量。通过fd系统可以找到对应的文件。

当我们创建多个文件时,fd是从3开始增长。为什么?
因为进程启动时会打开三个流,

stdin(标准输入)0 stdout(标准输出)1 stderr(标准输出错误)2

文件是存储在磁盘当中,当进程要访问文件时,文件才会从磁盘加载到内存。和进程一样,在内存中可以存在多个文件,这时候就进行管理。先描述,再组织。

每个加载到内存的文件都有struct file结构体,里面封装了与文件相关的信息,例如文件描述符、文件状态、指向文件内容的指针等。

struct file {
    int fd;                // 文件描述符
    const char *name;     // 文件名
    int flags;            // 打开标志(如只读、只写等)
    off_t pos;            // 当前读/写位置
    size_t size;          // 文件大小
    // 其他可能的字段,如文件类型、权限等
};

这些结构体用指针链接起来,形参file list链表。进程和文件的关系就变成task list和file list的关系。

在这中间起映射作用的就是files_struct,每个进程都有一个files_struct.

task_struct中有*files指向files_struct的指针,files_struct有指向所有打开文件的 file 结构体的数组。files_struct就是文件描述符表。文件描述符当作数组下标,来查找对应的struct file。

找到对应的struct file之后呢?

对于外部设备的管理,也是先描述,再组织。struct device有外设的信息,不同外设信息种类一样,但数据不同,进行io操作的方式也不同。比如,read wirte只能在键盘上读,wirte函数就实现为空,显示器只能输出,read就实现为空。不同外设都有相同的函数,但实现不同。

struct file中就可以定义函数指针,指向对应外设的io实现函数。

这样底层是不同的外设,但上层都是统一的struct file。

struct file和struct device就像父类和子类的关系,file里面都是函数指针,device 函数实现各不相同。

4.重定向

> 输出重定向 >>追加重定向 <输入重定向

Linux下重定向是怎么实现的?

我们知道进程是通过文件描述表中的指针数组,来找到对应的文件的。数组中每一个指针都指向一个文件,如果把数组中元素改变,让它指向其它文件,就完成了重定向。

没有指针指向的文件会自动关闭。

文件描述符的分配规则是从小到大分配,当close(1)时,再新打开一个文件,此时指向该文件的指针处于下标为1的位置。这也变相的实现的输出重定向。

dup2

关于重定向的操作有dup2函数来实现,dup2将一个文件描述符复制到另一个文件描述符。如果目标文件描述符已打开,它将被关闭并重新分配。

#include <unistd.h>

int dup2(int oldfd, int newfd);

oldfd要赋值的文件描述符,newfd被赋值的文件描述符

最后两个下标都指向oldfd指向的文件。

返回值:
成功 返回newfd的下标

失败 -1

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

int main() {
    int fd;

    // 打开文件以进行写入
    fd = open("output.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
    if (fd == -1) {
        perror("open");
        exit(EXIT_FAILURE);
    }

    // 重定向标准输出到文件描述符 fd
    if (dup2(fd, STDOUT_FILENO) == -1) {
        perror("dup2");
        exit(EXIT_FAILURE);
    }

    // 关闭原始文件描述符,因为不再需要
    close(fd);

    // 现在所有标准输出都会写入到 output.txt 文件
    printf("This will go to the output.txt file instead of the console.\n");

    return 0;
}

用户缓冲区和文件内核缓冲区

#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
int main()
{
    close(1);
    int fd = open("myfile", O_WRONLY | O_CREAT, 00644);
    if (fd < 0) {
        perror("open");
        return 1;
    }
    printf("fd: %d\n", fd);
    //fflush(stdout);
    close(fd);
    exit(0);
}

上述代码,close(1)把stdout向显示器打印的输出流关闭了,再创建并打开了myfile文件。根据文件描述符从小到大分配,所以此时向向显示器输出就重定向为向myfile文件输出。

所以这段代码执行完后,理应创建myfile并且里面有原本向显示器打印的内容。

[wws@hcss-ecs-178e myshell]$ ll
total 48
-rw-rw-r-- 1 wws wws    82 Oct 12 16:51 makefile
-rwxrwxr-x 1 wws wws 20128 Oct 16 21:02 myshell
-rw-rw-r-- 1 wws wws  7627 Oct 16 21:16 myshell.cc
-rwxrwxr-x 1 wws wws  8560 Oct 17 16:28 test
-rw-rw-r-- 1 wws wws   300 Oct 17 16:28 test.c
[wws@hcss-ecs-178e myshell]$ ./test
[wws@hcss-ecs-178e myshell]$ ll
total 48
-rw-rw-r-- 1 wws wws    82 Oct 12 16:51 makefile
-rw-r--r-- 1 wws wws     0 Oct 17 16:29 myfile
-rwxrwxr-x 1 wws wws 20128 Oct 16 21:02 myshell
-rw-rw-r-- 1 wws wws  7627 Oct 16 21:16 myshell.cc
-rwxrwxr-x 1 wws wws  8560 Oct 17 16:28 test
-rw-rw-r-- 1 wws wws   300 Oct 17 16:28 test.c
[wws@hcss-ecs-178e myshell]$ cat myfile
[wws@hcss-ecs-178e myshell]$ 

可以看到myfile被创建了,但里面并没有内容。为什么?

首先我们要理解fwirte把数据写到磁盘的过程。

1.它首先将数据写入用户缓冲区。当缓冲区满或用户请求写操作时(一般向显示器写行刷新,向文件写是缓冲区满了刷新),数据会被复制到内核缓冲区。然后,内核会将内核缓冲区的数据写入磁盘(由OS决定什么时候能刷新)。

2.struct FILE 

typedef struct {
    int fd;               // 文件描述符
    char *buffer;        // 缓冲区指针
    size_t buffer_size;  // 缓冲区大小
    size_t buffer_index; // 缓冲区的当前索引
    int flags;           // 文件状态标志
    long offset;         // 当前读写位置
    int error;           // 错误标志
    int eof;             // EOF 标志
    // 可能还有其他字段
} FILE;

3.fwirte和wirte区别,fwirte会先把数据写到FILE结构体的缓冲区 (用户缓冲区),而wirte直接把数据写到文件的内核缓冲区。

4.fclose close区别,fclose在关闭文件时它会自动处理与该文件流相关的缓冲区,会把用户缓冲区的数据刷新到内核缓冲区。而close不会,只会把内核缓冲区的数据刷新到磁盘。

5.fflush(FILE *stream) 把用户缓冲区的数据刷新到内核缓冲区

fsync(int fd),把文件的内核缓冲区中的数据写入磁盘中

所以原因就在于

1.因为把向显示器写重定向为向文件内写,导致用户缓冲区的刷新规则从行刷新变为满刷新。

2.fwirte是把内容写到了用户缓冲区

3.close(fd)会把文件的内核缓冲区的内容刷新到磁盘,但不会把用户缓冲区的内容进行刷新。

4.程序结束自动刷新缓冲区,但文件已经关闭 文件的内核缓冲区也关闭,用户缓冲区的数据不能刷新到内核缓冲区。也就导致不能向磁盘中写入。

解决方法:只要把要写入的数据在文件的内核缓冲区关闭前写入就可以
1.在文件关闭前 fflush(FLIE*)把数据从用户缓冲区刷新到内核缓冲区

2.不用fwirte,用wirte直接把数据写到文件的内核缓冲区中

3.不用close,用fclose文件关闭时,会把用户缓冲区的内容刷新

用户缓冲区的意义:fwirte把用户缓冲区写满才会刷新,对比wirte直接写到内核缓冲区这样的好处是1.减少了io交换 2.fwirte底层就是wirte,减少了系统调用 提高程序执行效率。

内核缓冲区也是为了提高io效率。

再来看一个例子

[wws@hcss-ecs-178e myshell]$ gcc test.c -o test
[wws@hcss-ecs-178e myshell]$ ./test
hello fwrite
hello fwrite
hello fprintf
hello write
[wws@hcss-ecs-178e myshell]$ ./test > file
[wws@hcss-ecs-178e myshell]$ cat file
hello write
hello fwrite
hello fwrite
hello fprintf
hello fwrite
hello fwrite
hello fprintf

为什么向显示器打印每个函数都打印一次,向文件打印只有wirte系统函数打印了一次呢?

1.重定向文件写入,导致行刷新变成满刷新。打印完后,缓冲区没满 不会进行刷新。

2.fork(),父进程的缓冲区会被复制到子进程中,但在子进程中进行的任何操作不会影响父进程的输出缓冲区。父子进程相互独立。

3.wirte直接写到内核缓冲区中,和FILE中的缓冲区没关系。

4.如果向显示器打印就是行刷新,等到fork后,用户缓冲区已经没有数据了。