【Linux操作系统】基础I/O(一)

1. 文件操作

1.1 写入文件

#include <stdio.h>

int main()
{
    FILE *fp = fopen("./log.txt", "w");
    if (fp == NULL)
    {
        perror("fopen");
        return 1;
    }

    const char* message = "I am ricky\n";
    int cnt = 5;
    while (cnt--)
        fputs(message, fp);

    fclose(fp);

    return 0;
}

1.2 读取文件

#include <stdio.h>

int main()
{
    FILE* fp = fopen("./log.txt", "w");
    if (fp == NULL)
    {
        perror("fopen");
        return 1;
    }

    const char* message = "I am ricky\n";
    int cnt = 5;
    while (cnt--)
        fputs(message, fp);

    fclose(fp);

    return 0;
}

1.3 打开文件的方式

  • r:Open text file for reading. The stream is positioned at the beginning of the file.
  • r+:Open for reading and writing. The stream is positioned at the beginning of the file.
  • w:Truncate(缩短) file to zero length or create text file for writing. The stream is positioned at the beginning of the file.
  • w+:Open for reading and writing. The file is created if it does not exist, otherwise it is truncated. The stream is positioned at the beginning of the file.
  • a:Open for appending (writing at end of file). The file is created if it does not exist. The stream is positioned at the end of the file.
  • a+:Open for reading and appending (writing at end of file). The file is created if it does not exist. The initial file position for reading is at the beginning of the file, but output is always appended to the end of the file.

1.4 stdin、stdout、stderr

在这里插入图片描述

发现这三个流的类型都是FILE*,故同样可以使用文件操作来操作这三个流,如:

#include <stdio.h>

int main()
{
    const char* msg = "hello, this is ricky\n";
    fputs(msg, stdout);

    return 0;
}

在这里插入图片描述

stdin:键盘、stdout:显示器、stderr:显示器

虽然stdout和stderr对应的设备都是显示器,都可以输出到显示器,但在使用>输出重定向的时候会有区别

  • stdout

    在这里插入图片描述

  • stderr

    在这里插入图片描述

因而>输出重定向的本质是把stdout的内容重定向到文件中

所有的这些文件操作最终都是要访问硬件的,而OS是硬件的管理者,因此所有的语言对“文件”的操作都必须贯穿操作系统,但是我们都知道OS不相信任何人,因而访问操作系统需要通过系统调用接口,故几乎所有的语言fopen、fclose、fread、fwrite、fgets、fputs、fgetc、fputc等底层一定需要使用OS提供的系统调用

2. 系统文件I/O

2.1 打开文件

头文件:#include <sys/types.h>#include <sys/stat.h>#include <fcntl.h>

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

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

int main()
{
   int fd = open("./log.txt", O_WRONLY | O_CREAT, 0644);
   if (fd < 0)
       printf("open failed\n");

   close(fd);

    return 0;
}

flags:传递标志位

在这里插入图片描述

这些都是只有一个比特位为1的数据,而且不会重复,这样就能很好的确定标志位是多少

返回值:

  • 0:标准输入
  • 1:标准输出
  • 2:标准错误
  • 3及之后的:打开的文件

所有的文件操作,表现上都是进程执行对应的函数,即进程对文件的操作,要想操作文件就必须先打开文件,然后将文件相关的属性信息加载到内存,操作系统中存在大量的进程(进程 : 打开的文件 = 1 : n)一个进程可以打开多个文件,因此系统中可能存在更多的打开的文件,那么操作系统就要把打开的文件在内存中管理起来(先描述,再组织)——struct file { //包含了打开文件的相关属性:连接属性… }

2.2 写入文件

头文件#include <unistd.h>

ssize_t write(int fd, const void *buf, size_t count);

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

int main()
{
    int fd = open("./log.txt", O_WRONLY | O_CREAT, 0644);
    if (fd < 0)
        printf("open failed\n");
    
    const char* str = "I am ricky!\n";
    int cnt = 5;
    while (cnt--)
        write(fd, str, strlen(str));


    close(fd);
    return 0;
}

注意:使用write写入文件时,不需要写入’\0’,以’\0’作为字符串的结束只是C的规定

2.3 读取文件

头文件#include <unistd.h>

ssize_t read(int fd, void *buf, size_t count);

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

int main()
{
    int fd = open("./log.txt", O_RDONLY);
    if (fd < 0)
    {
        perror("open");
        return 1;
    }

    char buffer[1024];
    ssize_t s = read(fd, buffer, sizeof(buffer) - 1);
    if (s > 0)
    {
        buffer[s] = '\0';
        printf("%s", buffer);
    }

    close(fd);
    return 0;
}

注意:如果要把读出来的内容当作一个字符串来处理,需要在最后加上’\0’,并且读取的时候要少读一个来放’\0’

2.4 关闭文件

头文件:#include <unistd.h>

int close(int fd);

3. 文件描述符fd

3.1 简述

当程序运行起来变成进程之后,默认情况下OS会帮助进程打开三个标准输入输出

  • 0:标准输入——键盘
  • 1:标准输出——显示器
  • 2:标准错误——显示器

open的返回值是系统给的,进程与打开文件的比是1 : n,操作系统内一定是打开了多个文件,因而OS必须对打开的文件进行管理,如果一个文件没有被打开,那么这个文件存储在磁盘里也要占磁盘空间,文件是属性,属性也是数据,磁盘文件 = 文件内容 + 文件属性,文件操作 = 对文件内容操作 + 对文件的属性操作

OS对打开的文件进行管理先描述再组织:struct file { \\文件的相关属性信息 }

在进程PCB的task struct中存在struct files_struct* files指向struct files_struct,让文件与进程之间产生关系

struct files_struct结构内包含一个数组struct file* fd_array[]为一个指针数组,依次指向打开文件的struct file

在这里插入图片描述

文件描述符的分配规则:给新文件分配的fd,是从fd_array中找一个最小的、没有被使用的,作为新的fd

如果我们close(0),则新打开的文件的fd = 0

3.2 关闭fd=1的情况

close(0)close(2)之后再printf打印都可以正常打印,但close(1)之后却没有输出,只在文件中显示

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

int main()
{
    close(1);
    int fd = open("./log.txt", O_CREAT | O_WRONLY, 0644);

    printf("fd: %d\n", fd);

    printf("hello world\n");
    printf("hello world\n");
    printf("hello world\n");
    printf("hello world\n");
    printf("hello world\n");
    printf("hello world\n");
    printf("hello world\n");
    printf("hello world\n");
    printf("hello world\n");
    printf("hello world\n");
    printf("hello world\n");

    return 0;
}

在这里插入图片描述

C中的printf本质是向标准输出打印,即stdout,而stdout是FILE*类型的

FILE在C语言层面上就是结构体,其中一定包含了一个整数,是对应在系统层面的,即这个文件打开对应的fd

stdout对应的fd=1,将其close之后重新指向“log.txt"文件

而语言层面的printf本底层是使用系统调用,他只会去操作fd=1对应的文件,在这里由stdout变成”log.txt",即完成了一次输出重定向

3.3 关闭fd=0的情况

输入重定向,将从键盘读入重定向为从文件读入

stdin的FILE结构体内对应的fd=0,这里将fd=0指向了“log.txt"

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

int main()
{
    close(0);
    int fd = open("./log.txt", O_RDONLY);

    char line[128];
    while (fgets(line, sizeof(line) - 1, stdin))
        printf("%s", line);
    
    return 0;
}

在这里插入图片描述

3.4 验证C库FILE结构体内部封装了fd
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>

int main()
{
    printf("stdin -> %d\n", stdin->_fileno);
    printf("stdout -> %d\n", stdout->_fileno);
    printf("stderr -> %d\n", stderr->_fileno);
    
    FILE* fp = fopen("./log.txt", "r");
    if (fp == NULL) {
        perror("fopen");
        return 0;
    }
    printf("fp -> %d\n", fp->_fileno);

    return 0;
}

在这里插入图片描述

3.5 dup2系统调用

头文件unistd.hint dup2(int oldfd, int newfd);

在这里插入图片描述

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

int main()
{
    int fd = open("./log.txt", O_WRONLY | O_TRUNC);
    if (fd < 0) {
        perror("open");
        return 1;
    }

    dup2(fd, 1); // 将本来应该显示到显示器的内容,写入到文件

    printf("hello printf\n");
    fprintf(stdout, "hello fprintf\n");
    fputs("hello fputs\n", stdout);

    return 0;
}
3.6 stdout与stderr的区别
#include <stdio.h>
#include <string.h>
#include <unistd.h>

int main()
{
    const char* msg1 = "hello stdout\n";
    write(1, msg1, strlen(msg1));

    const char* msg2 = "hello stderr\n";
    write(2, msg2, strlen(msg2));

    return 0;
}

上面这段代码,如果运行的话会正常输出

在这里插入图片描述

但是如果使用输出重定向的话,就只会有fd=1被重定向到文件里

在这里插入图片描述

其实重定向的本质是重定向标准输出

在这里插入图片描述

./redir > log.txt 2>&1:将stdout和stderr都重定向到“log.txt",2>$1就是将1拷贝到2,1在>之后指向"log.txt",拷贝后2也指向"log.txt"

3.7 缓冲区
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>

int main()
{
    close(1);
    int fd = open("./log.txt", O_CREAT | O_WRONLY, 0644);

    printf("fd: %d\n", fd);

    fprintf(stdout, "hello world\n");
    fprintf(stdout, "hello world\n");
    fprintf(stdout, "hello world\n");
    fprintf(stdout, "hello world\n");
    fprintf(stdout, "hello world\n");
    
    close(fd);

    return 0;
}

在这里插入图片描述

加上了close(fd)发现文件中不会被写入printf、fprintf的字符串,不加的时候能够写入

因为stdout是FILE*类型的,而FILE结构体内部维护了与C缓冲区相关的内容,那些输出的字符串会暂时保存在该缓冲区当中,然后通过fd=1刷新到文件

进程退出的时候,会刷新FILE内部的数据到OS缓冲区

用户到OS的刷新策略:

  • 立即刷新(不缓冲)
  • 行刷新(行缓冲——‘\n’)如:显示器打印
  • 缓冲区满了才刷新(全缓冲)如:往磁盘文件中写入

stdout由显示器重定向到文件,所以刷新方式也由行缓冲变为了全缓冲

因此在close(1)之前内容还在C语言缓冲区没有被写满,因此还没有刷新到OS缓冲区

然后在进程退出之前执行了close(1),将系统的文件描述符关闭,内容没有刷新的地方

但如果在close(1)之前执行fflush(stdout),将缓冲区内容刷新到操作系统内部,就没有影响了

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

int main()
{
    const char* msg1 = "hello stdout\n";
    write(1, msg1, strlen(msg1));

    const char* msg2 = "hello stderr\n";
    write(2, msg2, strlen(msg2));

    printf("hello printf\n");
    fprintf(stdout, "hello fprintf\n");

    close(1);
    
    return 0;
}

在这里插入图片描述

在使用>重定向之后stdout就变成了文件"log.txt",即fd=1指向的是"log.txt",write(1, ...)为系统调用接口没有C语言缓冲区

printffprintf指向文件的时候行缓冲策略会变成全缓冲不会立即刷新,内容会暂存在C缓冲区中,在进程退出之前close(1),缓冲区内容就无法刷新到文件中

总结:

stdout是FILE*类型的,FILE为一个结构体,里面维护了文件描述符fd以及C缓冲区buffer,其中缓冲区的刷新策略与目标文件的类别有关

内容会先拷贝到buffer当中,然后再由fd刷新到系统当中

3.8 FILE
#include <stdio.h>
#include <string.h>
#include <unistd.h>

int main()
{
    const char* msg1 = "hello stdout\n";
    write(1, msg1, strlen(msg1));

    printf("hello printf\n");
    fprintf(stdout, "hello fprintf\n");
    fputs("hello fputs\n", stdout);

    fork();
    
    return 0;
}

在这里插入图片描述

重定向到文件中重复出现的是使用C接口的时候,而系统调用接口并不受影响,即刷新策略变了

正常指向程序时,因为fork()在最后执行,因此在显示器上打印出四条语句是正常的

而重定向到文件时,printffprintffputs的刷新策略就变成了全缓冲,都会先暂存在FILE中的C缓冲区中,注意是C语言提供的缓冲区

即是父进程的缓冲区,fork()之后发生了写时拷贝,父子进程都向文件中刷新,故出现了重复刷新

如果要解决这个问题,可以在fork()之前先强制刷新缓冲区的内容,即fflush(stdout)

write()这个系统调用接口不会出现这样的情况,因此它是没有缓冲区的,这也验证了这个缓冲区是用户级别的

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Ricky_0528

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值