• 首页 首页 icon
  • 工具库 工具库 icon
    • IP查询 IP查询 icon
  • 内容库 内容库 icon
    • 快讯库 快讯库 icon
    • 精品库 精品库 icon
    • 问答库 问答库 icon
  • 更多 更多 icon
    • 服务条款 服务条款 icon

Linux文件基础IO操作C语言接口 | 系统调用接口 | 重定向

武飞扬头像
侠客cheems
帮助3

重修C语言文件知识

  1. 打开文件操作fopen函数:

学新通
我们看一段代码,以写(w)的形式来打开文件:

#include <stdio.h>

#define FILE_NAME "log.txt"
int main()
{
   FILE* fp = fopen(FILE_NAME,"w");
   if(NULL==fp)
   {
       perror("fopen");
       return 1;
   }
   fclose(fp);
   return 0;
}

一开始我们并没有创建文件,程序运行会自动创建一个log.txt的文件
学新通

  1. 打印输出到文件fprintf函数

学新通

#include <stdio.h>

#define FILE_NAME "log.txt"
int main()
{
   FILE* fp = fopen(FILE_NAME,"w");
   if(NULL==fp)
   {
       perror("fopen");
       return 1;
   }
   fprintf(fp,"%s\n","hello world!");
   fclose(fp);
   return 0;
}

运行程序,fprintf 会发送格式化输出到流 stream
学新通
w的方式对文件进行操作,文件的内容会被先清空,再进行操作
学新通

  1. 打开文件操作读(r)的方式,fgets函数从指定的流 stream 读取一行,并把它存储在 str 所指向的字符串内

学新通

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

#define FILE_NAME "log.txt"
int main()
{
   FILE* fp = fopen(FILE_NAME,"r");
   if(NULL==fp)
   {
       perror("fopen");
       return 1;
   }
   char buffer[64];
   while(fgets(buffer,sizeof(buffer)-1,fp) != NULL)
   {
       buffer[strlen(buffer)-1]=0;
       puts(buffer);
   }
   fclose(fp);
   return 0;
}
学新通

这个while循环可写可不写,这样写的作用也就是保证buffer最后一个字符是终止符号
下面输出的结果就是将log.txt中的数据输出出来
学新通

4.打开文件操作追加(a)的方式

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

#define FILE_NAME "log.txt"
int main()
{
   FILE* fp = fopen(FILE_NAME,"a");
   if(NULL==fp)
   {
       perror("fopen");
       return 1;
   }
   int count=5;
   while(count)
   {
       fprintf(fp,"%s:%d\n","hello world!!",count--);
   }
   fclose(fp);
   return 0;
}

学新通

学新通

打开文件相关方式:

  • “r” “只读”,只允许读取,不允许写入。文件必须存在,否则打开失败
  • “w” “写入”。如果文件不存在,那么创建一个新文件;如果文件存在,那么清空文件内容
  • “a” “追加”。如果文件不存在,那么创建一个新文件;如果文件存在,那么将写入的数据追加到文件的末尾(文件原有的内容保留)
  • “r ” “读写”。既可以读取也可以写入,也就是随意更新文件。文件必须存在,否则打开失败
  • “w ” “写入/更新”,相当于w和r 叠加的效果。既可以读取也可以写入,也就是随意更新文件。如果文件不存在,那么创建一个新文件;如果文件存在,那么清空文件内容
  • “a ” “追加/更新”,相当于a和r 叠加的效果。既可以读取也可以写入,也就是随意更新文件。如果文件不存在,那么创建一个新文件;如果文件存在,那么将写入的数据追加到文件的末尾(文件原有的内容保留)
  • “t” 文本文件。如果不写,默认为"t"。
  • “b” 二进制文件。

5.在linux下新建文件默认权限=0666,受到umask的影响,实际创建的出来的文件权限是: mask & ~umask
学新通
 C语言有文件的操作接口,那么C 、Java、python、php、GO等语言同样也有文件操作接口,但是它们的接口都不一样。
 而文件在哪?在磁盘,磁盘是硬件,而需要访问硬件都必须要操作系统OS来管理,使用OS给的文件级别的系统调用,操作系统只有一个,但是语言有很多个:库函数底层必须使用系统调用接口、库函数可以变化但是底层不变。


Linux文件知识

我们使用man 2来了解有关文件的系统调用知识,它与C语言有什么不同呢?

标记位传参

通过以下代码来解释什么是标记位传参:

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

void show(int flag)
{
    if(flag & ONE)printf("one\n");
    if(flag & TWO)printf("two\n");
    if(flag & THREE)printf("three\n");
    if(flag & FOUR)printf("four\n");
}
int main()
{
    show(ONE);
    printf("---------\n");
    show(TWO);
    printf("---------\n");
    show(ONE | TWO);
    printf("---------\n");
    show(ONE | TWO | THREE);
    printf("---------\n");
    show(ONE | TWO | THREE | FOUR);
    return 0;
}
学新通

标记对应比特位,每一个宏对应的数值,只有一个比特位是1,彼此不会重叠,如果想要互相结合就或(|),函数里面通过与(&)来判断,这样就相当于可以传入多个参数
学新通

文件的系统调用

1.系统调用打开文件open
学新通学新通
pathname: 要打开或创建的目标文件
flags: 打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行“或”运算,构成flags。
参数: 前三个常量,必须指定一个且只能指定一个

  • O_RDONLY: 只读打开

  • O_WRONLY: 只写打开

  • O_RDWR : 读写打开

  • O_CREAT : 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限

  • O_APPEND: 追加写

使用系统调用来打开文件O_WRONLY

#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#define FILE_NAME "log.txt"
int main()
{
    int fp = open(FILE_NAME,O_WRONLY);
    if(fp < 0)
    {
        perror("open");
        return 1;
    }
    close(fp);
    return 0;
}
学新通

在C语言中我们使用读的形式打开文件可以直接成功,但是在系统调用中我们使用只使用读的形式访问文件,是会失败的。
学新通
我们必须要更改代码为:

    int fp = open(FILE_NAME,O_WRONLY | O_CREAT);

更改后创建出来的文件是乱码:
学新通
凭什么认为Linux一创建文件就按照比如666、777去创建?我们在C语言中使用的是已经封装过的系统调用,他会自动生成权限,而系统调用没有这些东西,他需要自己去传参,所有我们最后还需要传入作为权限的参数:

    int fp = open(FILE_NAME,O_WRONLY | O_CREAT, 0666);

学新通
学新通

更改umask的值
学新通

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#define FILE_NAME "log.txt"
int main()
{
    umask(0);
    int fp = open(FILE_NAME,O_WRONLY | O_CREAT,0666);
    if(fp < 0)
    {
        perror("open");
        return 1;
    }
    close(fp);
    return 0;
}
学新通

我们在创建访问之前将umask的值更改为0,使用我们传入的权限值去初始化,最后这个log.txt的权限值就是666
学新通

我们再访问shell中umask的值发现它还是0002,这是为什么,我们不是刚刚已经修改了,而且创建出来的文件权限也是按照更改后的umask初始的?这是因为我们程序里面的umask是这个子进程在执行,而与shell这个父进程无关系,子进程只能改变自己的文件权限,所以我们更改的时候不会影响shell。
学新通

  1. 向文件写入write

学新通

ssize_t是有符号整型,在32位机器上等同与int,在64位机器上等同与long int.

open函数返回值:
我们以读的形式打开(或创建)文件,并打印出open函数的返回值

int main()
{
    umask(0);
    int fp = open(FILE_NAME,O_WRONLY | O_CREAT,0666);
    if(fp < 0)
    {
        perror("open");
        return 1;
    }
    printf("fp:%d\n",fp);
    close(fp);
    return 0;
}

为什么这个打印的值是3呢?且看目录中的理解什么是文件详细讲述
学新通

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

 读写文件有两种读写方案:文本类、二进制类(而这些文件读取的分类是语言本身提供的)
而操作系统就很简单粗暴,直接以void*方式返回,在操作系统看来都是二进制,操作系统只会管你要写几个字节,而不会管你具体内容(不管你是图片还是字符串什么的,只认二进制)

 假如我们也想像C语言那样写入hello word!! 并加上数字。一个是字符串、一个是数字,那么我们如何使用系统调用来实现呢?
学新通

#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#define FILE_NAME "log.txt"

int main()
{
    umask(0);
    int fp = open(FILE_NAME,O_WRONLY | O_CREAT,0666);
    if(fp < 0)
    {
        perror("open");
        return 1;
    }
    int count = 3;
    char arr[64];
    while(count)
    {
        sprintf(arr,"%s:%d\n","hello world!!",count--);
        write(fp,arr,strlen(arr));
    }
    close(fp);
    return 0;
}
学新通

我们使用sprintf将字符串写入数组中,然后使用write将数组写入文件
学新通
我们的write(fp,arr,strlen(arr));这个代码strlen不需要加1来存储\0,如果加了会出现乱码,因为以\0作为字符串的结束符是C语言规定的,和系统调用层面的文件操作没有关系

在C语言中,以写的方式打开文件会直接删除掉原数据,但是在系统调用的时候则是覆盖式的比如:
更改之前代码为:

sprintf(arr,"%s:%d\n","aaaaaa!!",count--);

学新通
我们在系统调用还需要传入一个O_TRUNC,才能实现出在C语言中w的效果:

    int fp = open(FILE_NAME,O_WRONLY | O_CREAT | O_TRUNC,0666);

学新通
学新通
追加就是将O_TRUNC跟换为O_APPEND

    int fp = open(FILE_NAME,O_WRONLY | O_CREAT | O_APPEND,0666);

学新通

  1. 读文件read函数

学新通

#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#define FILE_NAME "log.txt"

int main()
{
    umask(0);
    int fp = open(FILE_NAME,O_RDONLY);
    if(fp < 0)
    {
        perror("open");
        return 1;
    }
    char buffer[1024];
    ssize_t num = read(fp,buffer,sizeof(buffer)-1);
    if(num>0)buffer[num]=0;
    printf("%s",buffer);
    close(fp);
    return 0;
}
学新通

sizeof(buffer)-1这段作用是为填写终止符留出空间
buffer[num]=0;这个语句的作用是添加结束符(0、\0、NULL),因为在C语言中的函数会自己写,而系统调用需要我们自己去写
学新通
学新通

理解什么是文件

文件操作的本质:进程 被打开文件 的关系

进程可以打开多个文件么?答案是肯定的,进程很多,同样系统一定会存在大量的被打开文件,那么这些被打开文件肯定也需要被操作系统OS管理起来,先描述再组织 -> OS为了管理对应的打开文件,必定会为文件创建对应的内核数据结构标识文件:struct file{}(包含了大部分属性)

我们通过下面代码来理解文件:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#define FILE_NAME(number) "log.txt"#number//把参数转化为字符串,然后合起来
int main()
{
    int fd0 = open(FILE_NAME(1), O_WRONLY | O_CREAT | O_APPEND, 0666);
    int fd1 = open(FILE_NAME(2), O_WRONLY | O_CREAT | O_APPEND, 0666);
    int fd2 = open(FILE_NAME(3), O_WRONLY | O_CREAT | O_APPEND, 0666);
    int fd3 = open(FILE_NAME(4), O_WRONLY | O_CREAT | O_APPEND, 0666);
    int fd4 = open(FILE_NAME(5), O_WRONLY | O_CREAT | O_APPEND, 0666);
    
    printf("fd: %d\n", fd0);
    printf("fd: %d\n", fd1);
    printf("fd: %d\n", fd2);
    printf("fd: %d\n", fd3);
    printf("fd: %d\n", fd4);
    
    close(fd0);
    close(fd1);
    close(fd2);
    close(fd3);
    close(fd4);
    return 0;
}
学新通

通过下面的输出结果我们发现:为什么open返回值从3开始?012又去了哪里?连续的小整数,一般情况我们只在数组下标才有所对应
学新通
三个标准输入输出流:

  • stdin—键盘
  • stdout—显示器
  • stderr—显示器

FILE* fp = fopen();这个FILE是结构体,它里面有一个字段是文件描述符

学新通

我们增加下面代码:

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

从输出结果我们可以看出,012在哪是什么了:三个标准输入输出提前占用了012
学新通

为什么这些输出的数字是为数组下标?且看下图
学新通
通过上面的学习,我们知道了文件描述符就是一个小整数:

  • Linux进程默认情况下会有3个缺省打开的文件描述符,分别是标准输入0, 标准输出1, 标准错误2.
  • 0,1,2对应的物理设备一般是:键盘,显示器,显示器

上图可知:文件描述符就是从0开始的小整数。当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件。于是就有了file结构体。表示一个已经打开的文件对象。而进程执行open系统调用,所以必须让进程和文件关联起来。每个进程都有一个指针*files, 指向一张表files_struct,该表最重要的部分就是包涵一个指针数组,每个元素都是一个指向打开文件的指针!所以,本质上,文件描述符就是该数组的下标。所以,只要拿着文件描述符,就可以找到对应的文件

文件描述符的本质,就是数组的下标!

文件fd的分配规则

我们首先做一个结果分析:

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

#define FILE_NAME "log.txt"
int main()
{
    //close(0);
    umask(0);
    int fd = open(FILE_NAME,O_WRONLY | O_CREAT,0666);
    if(fd < 0)
    {
        perror("open");
        return 1;
    }
    printf("fd:%d\n",fd);
    close(fd);
    return 0;
}
学新通

打印的结果是3,这是我们上面解释过的
学新通

我们在main函数刚开始的地方先close(0);看看输出结果:
学新通

我们在main函数刚开始的地方先close(2);看看输出结果:
学新通

我们在main函数刚开始的地方先close(0);close(2);看看输出结果:
学新通

正常情况我们会自动打开三个标准输入输出,我们加载一个文件就会从3开始:
学新通
但是,如果我们关闭掉一个会怎么样呢?
学新通

如果我们关闭close(1);会出现什么情况呢?
学新通
我们发现,显示器上并没有打印的结果,这是为什么呢?且看下面解释:
学新通

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

这两个printf的结果都是一样:
学新通
学新通

上面结果可以说明printf本质上就是打印输出到stdout中,现在我们将stdout先关闭了,然后创建的新struct_file占据了原本的stdout的位置,所以我们可以认为现在的printf输出应该输出到新创建的文件log.txt中,但是我们cat log.txt发现里面并没有数据,难道我们的结论是错误的?不是的,这里是因为缓冲区的缘故。

我们在最后刷新一下fflush(stdout);,我们这里刷新的是stdout,且最后输出的结果就是fd:1,证明确实是分配的1号位
学新通

本来输出打印应该打印到stdout显示器上,但是我们现在关闭close(1);却将打印的结果打印到了新创建的文件中,这种特性叫做重定向

重定向

如果我们也想实现跟刚才一样的显示效果,将原本打印到显示器上的数据,输出到新建文件中去:
学新通
学新通
我们需要将1号位中的数据替换为新建文件fd,那么最终留下来的肯定是fddup2调用是将newfd中的内容替换为oldfd,留下来的是oldfd,所以我们使用dup2调用需要:dup2(fd,1);

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#define FILE_NAME "log.txt"
int main()
{
    umask(0);
    int fd = open(FILE_NAME,O_WRONLY | O_CREAT,0666);
    if(fd < 0)
    {
        perror("open");
        return 1;
    }
    dup2(fd,1);
    printf("fd:%d\n",fd);
    fprintf(stdout,"fd:%d\n",fd);
    fflush(stdout);
    close(fd);
    return 0;
}
学新通

这样我们就完成了一个重定向功能
学新通

追加重定向O_APPEND dup2(fd,1);

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#define FILE_NAME "log.txt"
int main()
{
    umask(0);
    int fd = open(FILE_NAME,O_WRONLY | O_CREAT | O_APPEND,0666);
    if(fd < 0)
    {
        perror("open");
        return 1;
    } 
    dup2(fd,1);
    printf("fd:%d\n",fd);
    fprintf(stdout,"fd:%d\n",fd);
    const char* msg = "hello world";
    write(1, msg, strlen(msg));
    fflush(stdout);
    close(fd);
    return 0;
}
学新通

持续运行./Test,将输出内容追加到log.txt
学新通

输入重定向dup2(fd,0);

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#define FILE_NAME "log.txt"
int main()
{
    umask(0);
    int fd = open(FILE_NAME,O_RDONLY);
    if(fd < 0)
    {
        perror("open");
        return 1;
    }
    dup2(fd,0);
    char arr[64];
    while(1)
    {
        printf("输入> ");
        if(fgets(arr, sizeof(arr), stdin) == NULL)break;
        printf("%s",arr);
    }
    close(fd);
    return 0;
}
学新通

通过dup2(fd,0);log.txt里面的数据用做标准输入,while循环的作用是将标准输入中的数据拿出来存放入数组中,然后将其打印出来
学新通

如果我们在父进程中创建子进程,然后这个子进程做重定向操作会影响父进程吗?
学新通
程序替换,同样不会影响曾经进程打开过的重定向文件,重定向的各种操作属于内核数据结果,而程序替换则是磁盘与内存的代码数据替换,二者不会影响到。

常见的重定向有:> >> <
 我们分别使用一下:

  1. <输出重定向
    学新通
  2. >输入重定向
    学新通
  3. >>追加重定向
    学新通

C语言文件层面的缓冲区知识

我们先运行下面代码:

#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main()
{
    // C语言接口
    printf("hello printf\n");
    fprintf(stdout, "hello fprintf\n");
    fputs("hello fputs\n", stdout);

    // 系统调用
    const char *msg = "hello write\n";
    write(1, msg, strlen(msg));

    return 0;
}
学新通

学新通
我们将输出的结果重定向到log.txt文件中,可以发现打印的结果是一样的
学新通
但是如果我们将代码更改一下,在结尾创建子进程:

int main()
{
    // C语言接口
    printf("hello printf\n");
    fprintf(stdout, "hello fprintf\n");
    fputs("hello fputs\n", stdout);

    // 系统调用
    const char *msg = "hello write\n";
    write(1, msg, strlen(msg));

    fork();
    //do nothing -> return quit
    return 0;
}

学新通
还是一样的操作,但是在我们创建子进程后的,将输出重定向到log.txt,我们发现C语言接口打印了两遍,而系统调用只打印了一次
学新通

缓冲区刷线策略问题:
 缓冲区一定会结合具体的设备,定制自己的刷新策略

  • 立即刷新 – 无缓冲
  • 行刷新 – 行缓冲 – 显示器
  • 缓冲区满 – 全缓冲 – 磁盘文件
  • 用户强制刷新,比如fflush
  • 进程退出 – 一般都要进行缓冲区刷新

上面的出现的现象,一定与缓冲区有关,且缓冲区一定不在内核中,因为如果在内核中,write也会打印两次。我们之前谈论过的缓存区,都是指的用户级语言层面给我们提供的缓冲区,我们之前的输出输出操作都要传入 -> stdout stdin stderr 它们都是FILE*类型的,而FILE是一个结构体,这个结构体里面封装了fd还有一个缓冲区,所以我们刷新缓冲区都是fflush(文件指针)fclose(文件指针)

解释上面现象:
代码结束之前,进行创建子进程
1.如果我们没有进行重定向>,看到了4条消息,stdout 默认使用的是行刷新,在进程fork之前,三条C函数已经将数据进行打印输出到显示器上(外设),我们的FILE内部,进程内部不存在对应的数据了
2.如果我们进行了重定向>,写入文件不再是显示器,而是普通文件,采用的刷新策略是全缓冲,之前的3条c显示函数,虽然带了\n,但是不足以让stdout缓冲区写满,数据并没有被刷新!!!
执行fork的时候,stdout属于父进程,创建子进程时,紧接着就是进程退出,谁先退出,一定要进行缓冲区刷新(就是修改),修改就会发生写时拷贝,数据最终会显示两份
3. write为什么没有呢?上面的过程都和wirte无关,wirte没有FILE,而用的是fd,就没有C提供的缓冲区


如有错误或者不清楚的地方欢迎私信或者评论指出🚀🚀

这篇好文章是转载于:学新通技术网

  • 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
  • 本站站名: 学新通技术网
  • 本文地址: /boutique/detail/tanhfeabcc
系列文章
更多 icon
同类精品
更多 icon
继续加载