linux之文件IO

1. C标准IO库函数

1.1 打开/关闭文件--fopen

• Makefile

复制代码
# 解释
#有时编译器不只是 gcc,我们将编译器定义为变量 CC,当切换编译器时只需要更改该变量的定义,而无须更改整个 Makefile。

# $@相当于当前 target 目标文件的名称,此处为 fopen_test。
# $^相当于当前 target 所有依赖文件列表,此处为 fopen_test.c
# ./$@的作用是执行目标文件

# rm ./$@的作用是在执行完毕后删除目标文件,如果没有这个操作,当源文件
#fopen_test.c 未更改时就无法重复执行,会提示:make:"fopen_test"已是最新。
#此处删除目标文件,使得我们在不更改源文件的情况下可以多次执行。

# 所有命令前都添加了"-"符号以忽略错误,确保即便上面的命令执行失败,仍然
#会向下执行。这样做是为了在发生错误时,确保删除目标文件,使得再次执行相同 target
#时不会提示:make:"fopen_test"已是最新,可以重新执行 target 下的命令。

CC := gcc

fopen_test: fopen_test.c
	-$(CC) $^ -o $@
	-./$@
-rm ./$@

• 例子

cpp 复制代码
#include <stdio.h>

int main(int argc, char const *argv[])
{
    /* code */

        /* 打开文件
	    char *__restrict __filename: 字符串表示要打开文件的路径和名称
	    char *__restrict __modes: 字符串表示访问模式
	    (1)"r": 只读模式 没有文件打开失败
	    (2)"w": 只写模式 存在文件写入会清空文件,不存在文件则创建新文件
	    (3)"a": 只追加写模式 不会覆盖原有内容 新内容写到末尾,如果文件不存在
	    则创建
	    (4)"r+": 读写模式 文件必须存在 写入是从头一个一个覆盖
	    (5)"w+": 读写模式 可读取,写入同样会清空文件内容,不存在则创建新文件
	    (6)"a+": 读写追加模式 可读取,写入从文件末尾开始,如果文件不存在则创建
	    return: FILE * 结构体指针 表示指向一个文件
	    FILE *fopen (const char *__restrict __filename,
	    const char *__restrict __modes)
    */

    FILE * fp = fopen("a.txt","w");
    if(fp == NULL){
        printf("打开失败\n");

    }else{
        printf("打开成功\n");

    }
    return 0;

1.2 fclose:

cpp 复制代码
#include <stdio.h>

int main(int argc, char const *argv[])
{
  
    FILE * fp = fopen("a.txt","w");
    if(fp == NULL){
        printf("打开失败\n");

    }else{
        printf("打开成功\n");

    }

    /*
        FILE *__stream: 需要关闭的文件
        return: 成功返回 0 失败返回 EOF(负数) 通常关闭文件失败会直接报错
        int fclose (FILE *__stream)
    */

    int result = fclose(fp);
    if(result == EOF)
        printf("关闭失败,%d\n",EOF);
    else if(result == 0)
        printf("关闭成功\n");

    return 0;
}

1.3 向文件写入数据的函数

1.3.1 fputc:一次只能写入一个字符。例子:

cpp 复制代码
#include <stdio.h>

int main(int argc, char const *argv[])
{
  
    FILE * fp = fopen("a.txt","a+");
    if(fp == NULL){
        printf("打开失败\n");

    }else{
        printf("打开成功\n");

    }

    /*写入文件一个字符
        int __c: 写入的 char 按照 AICII 值写入 可提前声明一个 char
        FILE *__stream: 要写入的文件,写在哪里取决于访问权限,在fopen参数那里
        return: 成功返回 char 的值 失败返回 EOF
        int fputc (int __c, FILE *__stream)
    */
    int put_result = fputc('A',fp);

    if(put_result == EOF){
        printf("写入失败,%d\n",EOF);
    }else {
        printf("写入%d成功\n",put_result);
    }


    int result = fclose(fp);
    if(result == EOF)
        printf("关闭失败,%d\n",EOF);
    else if(result == 0)
        printf("关闭成功\n");

    return 0;
}

1.3.2 fputs:一次写入一个字符串,例子:

cpp 复制代码
#include <stdio.h>

int main(int argc, char const *argv[])
{
  
    FILE * fp = fopen("a.txt","a+");
    if(fp == NULL){
        printf("打开失败\n");

    }else{
        printf("打开成功\n");

    }
   
    int putc_result = fputc('A',fp);

    if(putc_result == EOF){
        printf("写入失败,%d\n",EOF);
    }else {
        printf("写入%d成功\n",putc_result);
    }

    /*写入文件一个字符串
        char *__restrict __s: 需要写入的字符串
        FILE *__restrict __stream: 要写入的文件,写在哪里取决于访问权限
        return: 成功返回非负整数(一般是 0,1) 失败返回 EOF
        int fputs (const char *__restrict __s, FILE *__restrict __stream)
    */    


    int puts_result = fputs(" love B\n",fp);
    if(puts_result == EOF){
        printf("写入失败,%d\n",EOF);
    }else {
         printf("写入%d成功\n",puts_result);
    }


    int result = fclose(fp);
    if(result == EOF)
        printf("关闭失败,%d\n",EOF);
    else if(result == 0)
        printf("关闭成功\n");

    return 0;
}

1.3.3 fprintf:一次可以写很长的字符串,通过格式化输出的方式,输出一个长字符串,直接写入到文件,例子:

cpp 复制代码
#include <stdio.h>

int main(int argc, char const *argv[])
{
  
    FILE * fp = fopen("a.txt","a+");
    if(fp == NULL){
        printf("打开失败\n");

    }else{
        printf("打开成功\n");

    }
   
    int putc_result = fputc('A',fp);

    if(putc_result == EOF){
        printf("写入失败,%d\n",EOF);
    }else {
        printf("写入%d成功\n",putc_result);
    }

    int puts_result = fputs(" love B\n",fp);
    if(puts_result == EOF){
        printf("写入失败,%d\n",EOF);
    }else {
         printf("写入%d成功\n",puts_result);
    }
    
     /*
        FILE *__restrict __stream: 要写入的文件,写在哪里取决于访问模式
        char *__restrict __fmt: 格式化字符串
        ...: 变长参数列表
        return: 成功返回正整数(写入字符总数不包含换行符) 失败返回 EOF
        fprintf (FILE *__restrict __stream, const char *__restrict
        __fmt, ...)
    */

    int s_len = fprintf(fp,"床前明月光,\n疑是地上霜。\n举头望明月,\n低头思故乡        \n\t\t%s\n","李白");
    if(s_len == EOF){
        printf("写入失败\n");
    }else {
         printf("写入成功\n");
    }

    int result = fclose(fp);
    if(result == EOF)
        printf("关闭失败,%d\n",EOF);
    else if(result == 0)
        printf("关闭成功\n");

    return 0;
}

1.4 从文件中读取数据的函数

1.4.1 fgetc:一次读一个字符(字节),例子:

cpp 复制代码
#include <stdio.h>

int main(int argc, char const *argv[])
{
    FILE * fp = fopen("a.txt","r");
    if(fp == NULL)
        printf("文件打开失败\n");

    /*
        FILE *__stream: 需要读取数据的那个文件
        return: 读取的一个字节 到文件结尾或出错返回 EOF
        int fgetc (FILE *__stream)
    */

    //假如要读取的是中文,在UTF-8中普通的中文一般占3个字节,一些生僻字可能占4个
    //那么如果用fgetc读取,一次只能读取一个字节,就是说读不完整。需要连续读取(中间不能有换行)
    //char c = fgetc(fp);
    //printf("%c",c);
    char c = fgetc(fp);
    while (c != EOF)
    {
        printf("%c",c);
        c = fgetc(fp);
       
    }
    

    int result = fclose(fp);
    if(result == EOF)
        printf("关闭文件失败\n");

    return 0;
}

1.4.2 fgets:一次读取一行字符串,并且读取一行的数据长度是有限制的,例子:

cpp 复制代码
#include <stdio.h>

int main(int argc, char const *argv[])
{
    FILE * fp = fopen("a.txt","r");
    if(fp == NULL)
        printf("文件打开失败\n");


    /*
        char *__restrict __s: 接收读取的数据字符串
        int __n: 能够接收数据的长度
        FILE *__restrict __stream: 需要读取的文件
        return: 成功返回字符串 失败返回 NULL(可以直接用于 while)
        fgets (char *__restrict __s, int __n, FILE *__restrict __stream)
    */
    char buff[128];
    //char * readBuf = fgets(buff,sizeof(buff),fp);
    
    while (fgets(buff,sizeof(buff),fp) != NULL)
    {
        //printf("读取出来的数据是:%s\n",buff);
        printf("%s",buff);
    }
    //printf("读取出来的数据是:%s\n",readBuf);
    //printf("读取出来的数据是:%s\n",buff);
    
    

    int result = fclose(fp);
    if(result == EOF)
        printf("关闭文件失败\n");

    return 0;
}

1.4.3 fscanf:按指定格式从文件读取数据,例子:

cpp 复制代码
#include <stdio.h>

int main(int argc, char const *argv[])
{
    FILE * fp = fopen("user.txt","r");
    if(fp == NULL)
        printf("文件打开失败\n");


    /*
        FILE *__restrict __stream: 读取的文件
        char *__restrict __format: 读取的字符串
        ...: 变长参数列表 用于接收匹配的数据,
        return: 成功返回参数的个数 失败 返回 0报错 或结束返回 EOF
        int fscanf (FILE *__restrict __stream, const char *__restrict
        __format, ...)
    */
    char name[128];
    int age;
    char wife[128];
    // int count = fscanf(fp,"%s %d %s",name,&age,wife);//出现空行会自动跳过
    // if(count != EOF){
    //     printf("成功配对的个数是%d\n",count);
    //     printf("%s %d %s\n",name,age,wife);
    // }
    while (fscanf(fp,"%s %d %s",name,&age,wife) != EOF)
    {
         printf("%s %d %s\n",name,age,wife);
    }
    int result = fclose(fp);
    if(result == EOF)
        printf("关闭文件失败\n");

    return 0;
}

1.5 标准输入,输出,错误

cpp 复制代码
#include<stdio.h>
#include<stdlib.h>


int main(){
    //从标准输入里面读取数据
    char * buf = malloc(10);

    //stdin: 标准输入 FILE *
    fgets(buf,10,stdin);
    printf("从标准输入读到的数据是%s\n",buf);


    //从标准输出 里面 输出数据

    //stdout: 标准输出 FILE * 这个文件流会将数据输出到控制台
    //printf 底层就是使用的这个
    fputs("baiziyan zhen shuai!",stdout);
    printf("\n");

    //错误输出
    //错误输出 FILE * 一般输出到 错误日志
    fputs("baiziyan zhen shuai!",stderr);
    return 0;
}

1.6 例子

cpp 复制代码
配置文件的修改
如: 
	SPEED=5
	LENG=100
	SCORE=90
    LEVEL=95
cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>


int main(int argc, char const *argv[])
{
    FILE * fp = fopen("config.txt","r");
    FILE * fp1 = fopen("config1.txt","w");
   
    char readbuf[100];
    char res[100];
    int i = 0;
    while (fgets(readbuf,sizeof(readbuf),fp) != NULL){
        if(strstr(readbuf,argv[1]) != NULL){
            printf("读取到的数据为:%s\n",readbuf);
            char * rres = strtok(readbuf,"=");//分割字符串,获取键名(如"LEVEL")
            sprintf(res,"%s=%s\n",rres,argv[2]);//拼接新字符串
            printf("拼接之后的数据为%s\n",res);
            fputs(res,fp1);
            
        }else{
            fputs(readbuf,fp1);//一行字符串,原样写入
        }
    }
    fclose(fp);
    fclose(fp1);
    
    system("mv config1.txt config.txt");//覆盖

    return 0;
}
//./xxx.exe LEVEL 50

2. 系统调用

• 系统调用 是 操作系统内核 提供 给应用程序,使其可以间接访问硬件资源的接口

2.1 open:系统调用用于打开一个标准的文件描述符

cpp 复制代码
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>


int main()
{
    /*
        const char *__path: 文件路径
        int __oflag: 用于指定打开文件的方式,可以是以下选项的组合:
            (1) O_RDONLY: 以只读方式打开文件
            (2) O_WRONLY: 以只写方式打开文件
            (3) O_RDWR: 以读写方式打开文件
            (4) O_CREAT: 如果文件不存在,则创建一个新文件
            (5) O_APPEND: 将所有写入操作追加到文件的末尾
            (6) O_TRUNC: 如果文件存在并且以写入模式打开,则截断文件长度为 0
                还有其他标志,如 O_EXCL(当与 O_CREAT 一起使用时,只有当文件不存在时才创建新文件)
                可选参数: mode -> 仅在使用了 O_CREAT 标志且文件尚不存在的情况下生效,用于
                指定新创建文件的权限位 权限位通常由三位八进制数字组成,分别代表文件所有者、同组
                用户和其他用户的读写执行权限
        return: (1) 成功时返回非负的文件描述符。
                   (2) 失败时返回-1,并设置全局变量 errno 以指示错误原因。
*/
    //linux操作系统有文件权限的保护,就是其他用户没有写权限。
    int fd = open("b.txt",O_RDWR | O_CREAT,0757);
    if(fd == -1){
        printf("文件打开失败\n");
    }

    return 0;
}

2.2 read: 系统调用 用于读取已经打开的文件描述符

cpp 复制代码
#include <unistd.h> 
/* 
int __fd:一个整数,表示要从中读取数据的文件描述符 
void *__buf:一个指向存数据的缓冲区的指针,读取的数据将被存放到这个缓冲区中 
size_t __nbytes:一个 size_t(long类型) 类型的整数,表示要读取的最大字节数 系统调用将 
	                 尝试读取最多这么多字节的数据,但实际读取的字节数可能会少于请求的数量 
return: (1) 成功时,read()返回实际读取的字节数 这个值可能小于__nbytes,如果遇到了文件结尾(EOF)或者因为网络读取等原因提前结束读取 
           (2) 失败时,read()将返回-1 
*/

2.3 write:系统调用用于 对打开的文件描述符写入数据

cpp 复制代码
#include <unistd.h> 
/* 
int __fd:一个整数,表示要写入数据的文件描述符 
void *__buf:一个指向缓冲区的指针,写入的数据需要先存放到这个缓冲区中 
size_t __n:一个 size_t 类型的整数,表示要写入的字节数 write()函数会尝试写 
	         入n 个字节的数据,但实际写入的字节数可能会少于请求的数量 
return: (1) 成功时,write()返回实际写入的字节数 这个值可能小于__n,如果写 
	           入操作因故提前结束,例如: 磁盘满、网络阻塞等情况 
            (2) 失败时,write()将返回-1 
*/

2.4 close:系统调用用于在使用完成之后,关闭对文件描述符的引用

cpp 复制代码
#include <unistd.h> 
/* 
int __fd:一个整数,表示要关闭的文件描述符 
return: (1) 成功关闭时 返回 0 
            (2) 失败时 返回-1 
*/

2.5 exit和_exit

2.5.1 系统调用_exit

• _exit()是由 POSIX 标准定义的系统调用,用于立即终止一个进程,定义在unistd.h 中。这个调用确保进程立即退出,不执行任何清理操作。

• _exit()在子进程终止时特别有用,这可以防止子进程的终止影响到父进程(比如,防止子进程意外地刷新了父进程未写入的输出缓冲区)。

• _exit 和_Exit 功能一样。

cpp 复制代码
#include <unistd.h> 
/** 
* 立即终止当前进程,且不进行正常的清理操作,如关闭文件、释放内存等。这个函数 
   通常在程序遇到严重错误需要立即退出时使用,或者在某些情况下希望避免清理工作时调用。 
*
* int status: 父进程可接收到的退出状态码 0 表示成功 非 0 表示各种不同的错误 
*/ 
void _exit(int status); 
void _Exit (int __status) ;

2.5.2 库函数exit

cpp 复制代码
#include <stdlib.h> 
/** 
* 终止当前进程,但是在此之前会执行 3 种清理操作 
* (1) 调用所有通过 atexit()注册 的 终止处理函数(自定义) 
* (2) 刷新所有标准 I/O 缓冲区(刷写缓存到文件) 
* (3) 关闭所有打开的标准 I/O 流(比如通过 fopen 打开的文件) 
* 
* int status: 父进程可接收到的退出状态码 0 表示成功 非 0 表示各种不同的错误 
*/ 

2.6 例子

cpp 复制代码
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>


int main()
{
    int fd = open("a.txt",O_RDONLY);
    if(fd == -1){
        perror("open");
        exit(EXIT_FAILURE);
    }

    char buf[1024];
    ssize_t n_read ;
    n_read = read(fd,buf,sizeof(buf));
    if(n_read == -1){
        perror("read");
        close(fd);
        exit(-1);
    }

    write(STDOUT_FILENO,buf,n_read);

    close(fd);
    return 0;
}

3. 文件描述符

3.1 定义

• 在 Linux 系统中,当我们打开或创建一个文件(或套接字)时,操作系统会提供一个文件描述符,这是一个非负整数,我们可以通过它来进行读写等作。

• 文件描述符本身 只是 操作系统给应用程序 操作 底层资源(如文件、套接字等)所提供的一个引用或"句柄 ",其实也可以说文件描述符是一个索引。

• 在 Linux 中,文件描述符 0、1、2 是有特殊含义的。

• 0 是标准输入(stdin)的文件描述符。

• 1 是标准输出(stdout)的文件描述符。

• 2 是标准错误(stderr)的文件描述符。

3.2 文件描述符引用的图解,每个进程都只存自己的文件描述符表。如图:

3.3 每个文件描述符 都关联到 内核一个 struct file 类型的结构体数据

• f_count:多少个 fd/进程在引用这份 file(这个文件),降到 0 时释放对象。

• f_pos:当前文件位置(读写位置)。

• f_path:记录文件的路径。

• f_inode:指向与文件相关联的 inode 对象的指针,该对象用于维护文件元数据,如文件类型、访问权限等。

•const struct file_operations *f_op; // 指向文件操作函数表的指针,定义了文件支持的操作,如读、写、锁定等。

• private_data:存储别的数据。

• 小结:

当我们执行 open()等系统调用时,内核会创建一个新的 struct file(文件描述的结构体),这个数据结构记录了文件的元数据(文件类型、权限等)、文件路径、支持的操作等,然后分配文件描述符,将 struct file 维护在文件描述符表中,最后将文件描述符返回给应用程序。 我们可以通过文件描述符 对文件执行它所支持的各种函数进行操作,而这些函数的函数指针都维护在struct file_operations 数据结构中。文件描述符实质上是底层数据结构 struct file 的一个引用或者句柄,提供了操作底层文件的入口。

• 补充,如图:

3.4 例子,实现linux cp命令的代码

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>


int main(int argc, char const *argv[])
{
    int fd = open(argv[1],O_RDONLY);
    if(fd == -1){
        perror("open");
        close(fd);
        exit(EXIT_FAILURE);
    }
    off_t move = lseek(fd,0,SEEK_END);
    printf("偏移量为%ld\n",move);
    char * buf = malloc(sizeof(char) * move + 8);
    lseek(fd,0,SEEK_SET);

    ssize_t n_read = read(fd,buf,move);

    if(n_read < 0){
        perror("read");
        close(fd);
        exit(EXIT_FAILURE);
    }else{
        int fd1 = open(argv[2],O_WRONLY | O_CREAT | O_TRUNC);
        ssize_t n_write = write(fd1,buf,n_read);
        close(fd1);

    }
    close(fd);

    return 0;
}

4. 系统调用和fopen等这些的区别

• 来源的不同:

• fopen是来自是ANSIC标准中的C语言库函数,在不同的系统中应该调用不同的内核api。返回的是一个指向文件结构的指针。

• 系统调用的open :是由UNIX系统调用函数(包括LINUX等),返回的是文件描述符(File Descriptor),它是文件在文件描述符表里的索引。

• 移植性:

• fopen是C标准函数,因此拥有良好的移植性。

• open是由UNIX系统调用,移植性有限。如windows下相似的功能使用API函数CreateFile。

• 使用范围:

• open 返回的是文件描述符 ,而文件描述符是UNIX系统下的一个重要概念,UNIX下的一切设备都是以文件的形式操作。如网络套接字、硬件设备等。当然包括操作普通正规文件(Regular File)。

• fopen是用来操纵普通正规文件(Regular File)的。

• 文件IO层次:如果从文件IO的角度来看,前者属于低级IO函数,后者属于高级IO函数。低级和高级的简单区分标准是:谁离系统内核更近。低级文件IO运行在内核态,高级文件IO运行在用户态

• 缓冲区:

• 使用fopen函数,由于在用户态下就有了缓冲,因此进行文件读写操作的时候就减少了用户态和内核态的切换(切换到内核态调用还是需要调用系统调用API:read,write)。

• 使用open函数,在文件读写时则每次都需要进行内核态和用户态的切换。

相关推荐
Trouvaille ~2 小时前
【Linux】UDP Socket编程实战(一):Echo Server从零到一
linux·运维·服务器·网络·c++·websocket·udp
嵌入小生0072 小时前
Shell | 命令、编程及Linux操作系统的基本概念
linux·运维·服务器
-Try hard-3 小时前
Linuv软件编程 | Shell命令
linux·运维·服务器
释怀不想释怀3 小时前
Linux快捷键,软件安装启动
linux·运维·服务器
Hello World . .3 小时前
Linux:软件编程
linux·运维·服务器·vim
人间打气筒(Ada)4 小时前
k8s:CNI网络插件flannel与calico
linux·云原生·容器·kubernetes·云计算·k8s
老师用之于民4 小时前
【DAY21】Linux软件编程基础&Shell 命令、脚本及系统管理实操
linux·运维·chrome·经验分享·笔记·ubuntu
江畔何人初5 小时前
pod的内部结构
linux·运维·云原生·容器·kubernetes
不做无法实现的梦~5 小时前
PX4编译环境配置和qgc配置安装教程(2)
linux·stm32