【Linux】基础I/O----C语言文件操作与系统调用文件操作

目录

前言

书接上文【Linux】进程控制(二)----进程程序替换、编写自主Shell命令行解释器(简易版)详情请点击查看,今天继续介绍【Linux】基础I/O----C语言文件操作与系统调用文件操作

一、文件知识补充

  1. Linux下一切皆文件(键盘、显示器、网卡、磁盘......),文件 = 内容 + 属性(元数据),对文件的操作就是对文件内容或者文件属性做操作
  2. 访问一个文件,必须先将文件打开。访问文件就是对文件做增删查改,通过冯诺依曼体系我们可以知道CPU和内存打交道,不直接和输入、输入进行数据传输,因此访问文件时,要将文件加载到内存中
  3. 没有加载到内存中的文件,默认保存在磁盘中。我们对文件的学习就是学习被打开的文件和未被打开的文件
  4. 对于0KB的空文件是占用磁盘空间的
  5. 磁盘是永久性存储介质,因此文件在磁盘上的存储是永久性的;磁盘是外设(即是输出设备也是输入设备)
  6. 磁盘上的文件本质是对文件的所有操作,都是对外设的输入和输出,简称IO
  7. 用户通过bash,启动进程,让进程通过操作系统(只有操作系统才能访问到磁盘)将文件打开,因此打开文件底层一定有使用系统调用打开文件
  8. OS中,一定存在大量被打开的文件,因此操作系统需要管理这些打开的文件(先描述,再组织),一定存在一个结构体描述被打开的文件,如同PCB结构一样
  9. 进程有task_struct,进程也会有打开的文件,我们研究被打开文件本质 就是研究:进程与文件的关系

二、回顾C文件接口

C语言接口打开文件

  1. 下面是使用C语言打开文件(fopen接口)的代码
cpp 复制代码
#include <stdio.h>
int main()
{
	 FILE *fp = fopen("myfile", "w");
	 if(!fp)
	 {
	 		perror("fopen");
	 		return 1;
	 }
	 fclose(fp);
	 return 0;
}
  1. fopen函数是C打开文件的接口,传入参数文件名、打开文件模式
  2. 打开文件模式为r、r+、w、w+、a、a+
  3. 当fopen函数调用成功返回一个FILE指针 (C语言提供的数据类型),失败返回NULL并设置错误码
  4. fclose函数
  1. 我们在上面代码基础上打印进程pid,并让进程死循环,监测进程
cpp 复制代码
#include <stdio.h>
int main()
{
	 printf("我是一个进程, pid : %d\n", getpid());
	 FILE *fp = fopen("myfile", "w");
	 if(!fp)
	 {
	 		perror("fopen");
	 		return 1;
	 }
	 while(1)
	 {
	 		sleep(1);
	 }
	 fclose(fp);
	 return 0;
}
  1. 使用 ls /proc/[进程id] -l 命令查看当前正在运行进程的信息,我们可以看到该进程中有cwd,这就是为什么我们fopen的第一个参数只传入文件名,会自动在当前工作路径目录下(/home/gy/117/code/linux/lesson5)新建文件:/home/gy/117/code/linux/lesson5/myfile
  2. 因此我们更改当前进程的工作路径,那么新建只有文件名的文件就会新建到指定路径:chdir

  • 从下面截图可以看到cwd变为了/home/gy/117,在/home/gy/117路径下也看到了我们创建的文件myfile
  1. 因此,打开一个文件必须要文件路径+文件名

C语言接口写文件

  • C语言写入接口有很多:fwrite、fputs、fputc,我们这里以fwrite来进行写入
  • const void *ptr:传入的写入数据
  • size_t size:写入时的基本单元
  • size_t nmemb:写入几个基本单元

  • 下面代码,我们向myfile文件中写入一个字符串,C语言字符串结尾都是以'\0'结尾的,我们在传入写入单元大小时需不需要strlen() + 1呢?
  • 答案是不需要,因此字符串以'\0'结尾是C语言的规定,如果写入到文本文件中,会乱码。所以不需要写入到文件中,当我们将数据从文件读取过来的时候,再在末尾加上'\0'即可
cpp 复制代码
#include <stdio.h>
int main()
{
	 printf("我是一个进程, pid : %d\n", getpid());
	 FILE *fp = fopen("myfile", "w");
	 if(!fp)
	 {
	 		perror("fopen");
	 		return 1;
	 }
	 const char* str = "hello linux\n";
	 fwrite(str, strlen(str), 1, fp);
	 fclose(fp);
	 return 0;
}

我们发现:写入文件中,每次都是先将文件清空,再写入

  1. w:以w方式打开,如果文件存在,会先清空文件内容再做写入;如果没有该文件,创建文件,再做写入
  2. a:以a方式打开,如果文件存在,会直接在文件末尾写入,不做清空;如果没有该文件,创建文件,再做写入
  3. w+:以w+方式打开读和写,如果文件不存在,新建文件再读写(文件开头位置读写)
  4. a+:以a+方式打开读和写,如果文件不存在,新建文件,初始读的时候在文件的开始,写在文件末尾追加写入
  5. r:以r方式打开,读操作,从文件开头处读
  6. r+:以r+方式打开读和写,从文件开始
  • 注意:一般文件读写不会同时打开,因为文件读写有读写位置。文件有文本文件和二进制文件,我们可以将文件看作一维数组,文件的读写位置就是数组下标:从文件的开头位置读写,就是从数组0下标开始,在文件末尾追加写入,就是从数组末尾写入

echo重定向

  • 之前我们学习了echo 重定向和追加重定向到某个文件中
  • echo "123" > myfile:我们发现myfile中数据由hello linux -> 123,再次echo "123" > myfile,文件中依旧是123> myfile文件直接被清空了。
  • echo在命令行输入之后,变为一个进程,重定向到myfile,一定要将文件打开(w方式打开)
  • echo "123" >> myfile:追加重定向(以a方式打开)

C语言接口读文件

  • fread接口读取文件
  • fread和fwrite的返回值比较特殊,返回值是读取或写入了几个基本单元大小


  • feof函数:检测是否读取到文件结尾
cpp 复制代码
#include <stdio.h>
int main()
{
	 printf("我是一个进程, pid : %d\n", getpid());
	 FILE *fp = fopen("myfile", "r");
	 if(!fp)
	 {
	 		perror("fopen");
	 		return 1;
	 }
	while(1)            
  {            
       char buffer[1024];
       buffer[0] = 0; //将数组清空     
       size_t n =  fread(buffer, 1, sizeof(buffer) - 1, fp);
       if(n > 0)
       {      
           buffer[n] = 0;
           printf("%s", buffer);  
       }                                                    
       if(feof(fp))                                                    
       {      
           break;
       }                                                         
    }   
	 fclose(fp);
	 return 0;
}
  • cat命令底层就是使用的读文件操作来实现的

三、stdin/stdout/stderr

补充知识

  1. 向显示器写入123,写入的是一个一个字符'1'、'2' 、'3',而不是写入的int类型的123。所以显示器是字符设备
  2. 通过键盘输入的也是一个一个字符'1'、'2' 、'3',而不是int类型123,所以键盘也是字符设备
  3. 这也是在C语言/C++中我们打印字符时要指明类型printf("%d", x):是将整型变量x = 123,变为字符'1'、'2' 、'3',再使用int putchar(int c)将字符格式化打印在显示器上,所以printf函数是格式化输出int a = 0; scanf("%d", &a):键盘输入的是字符'1'、'2' 、'3',scanf函数将在键盘中获得的字符转换为整型123,再将其保存在a的空间中,所以scanf函数是格式化输入
  4. 显示器、键盘是文本文件,二进制文件就是不需要 进行格式化工作、能直接保存的文件

stdin&stdout&stderr

  • 进程启动时,会默认打开三个输入输出流(就是打开3个文件),分别是stdin、stdout、stderr(全局),这三个流的类型都是FILE*
  • stdin:标准输入->键盘文件
  • stdout:标准输出->显示器文件
  • stderr:标准错误->显示器文件

为什么要默认打开stdin、stdout、stderr

  • 一个进程被调度,就是要使用CPU资源进行计算的,因此进程就需要从键盘获得数据,计算完之后,输出计算结果,计算出错了还得有输出错误,所以进程要默认打开stdin、stdout、stderr

输出信息到显示器上方法

printf、fprintf函数

cpp 复制代码
int main()
 {
     const char* s1 = "hello printf\n";
     printf(s1);
 
     const char* s2 = "hello fprintf\n";
     fprintf(stdout, s2);
     return 0;
}

fputs函数

cpp 复制代码
int main()
 {
     const char* s1 = "hello printf\n";
     printf(s1);
 
     const char* s2 = "hello fprintf\n";
     fprintf(stdout, s2);
     
     const char* s3 = "hello fputs\n";
     fputs(s3, stdout);
     return 0;
}

fwrite函数

  • 因为向显示器打印就是向stdout中写入,stdout也是文件,也就可以调用文件写入接口,stdout也是FILE*类型
cpp 复制代码
int main()
 {
     const char* s1 = "hello printf\n";
     printf(s1);
 
     const char* s2 = "hello fprintf\n";
     fprintf(stdout, s2);
     
     const char* s3 = "hello fputs\n";
     fputs(s3, stdout);
     
     const char* s4 = "hello fwrite\n";
     fwrite(s4, strlen(s4), 1, stdout);
     return 0;
}

四、系统调用接口实现文件的打开、写入

open函数

  • 实际上打开一个文件,由于文件在磁盘中保存,只有操作系统才能管理磁盘,打开磁盘中文件,因此打开文件底层需要系统调用,我们学习的C语言打开文件接口其底层都是调用的open函数
  • 当打开文件存在是调用两个参数的open函数;当打开文件不存在时,调用三个参数的open函数,因此文件不存在需要先创建文件,创建文件就要这是文件权限(r、w、x)
  • 打开方式(flags):O_APPEND、O_CREAT、O_RDONLY(只读)、O_WRONLY(只写)、O_RDWR、O_TRUNC
  • flags是一个整数,一个整数有32个比特位,O_APPEND、O_CREAT...这些是宏,各自代表一个数字,每个数字都只有一个比特位是1

设计一个宏

  • 我们实现一个版本显示函数,设置不同版本的宏VERSION1 (1 << 0)、VERSION2 (1 << 1).....,通过函数ShowVersion传入宏来实现对应的显示
cpp 复制代码
#include <stdio.h>                   
#include <unistd.h>                  
#include <string.h>                  
                                    
#define VERSION1 (1 << 0) // 1            000001 
#define VERSION2 (1 << 1) // 2            000010
#define VERSION3 (1 << 2) // 4            000100
#define VERSION4 (1 << 3) // 8            001000
#define VERSION5 (1 << 4) // 16           010000
                                     
void ShowVersion(int flags)          
{                                    
    if(flags & VERSION1)             
        printf("VERSION1\n");        
    if(flags & VERSION2)             
        printf("VERSION2\n");        
    if(flags & VERSION3)             
        printf("VERSION3\n");        
    if(flags & VERSION4)             
        printf("VERSION4\n");        
    if(flags & VERSION5)             
        printf("VERSION5\n");        
}                                    
int main()                           
{                                    
    ShowVersion(VERSION1); //显示版本
    printf("---------------------------\n");
    ShowVersion(VERSION1 | VERSION2); //显示版本
    printf("---------------------------\n");
    ShowVersion(VERSION1 | VERSION3 | VERSION4); //显示版本                                        
    printf("---------------------------\n");                             
    return 0;                                                            
}           

通过传入不同的标志位来实现不同的显示,传入多个宏(用 | 连接)实现多个标志位的显示

使用open函数创建一个文件

  • 系统调用关闭文件函数close
  • 我们在打开文件时,如果文件不存在就需要默认创建该文件,则需要传入宏O_CREAT:表示创建文件
cpp 复制代码
int main()
{
    int fd = open("log.txt", O_WRONLY | O_CREAT);
    if(fd < 0)
    {
        perror("open");
        return 1;
    }
    close(fd);                                                                                     
    return 0;
}   

这样创建出来一个文件,但是该文件权限乱码,所以我们新建文件时,必须传入权限 ,这里将文件权限设置为0666

cpp 复制代码
int main()
{
    int fd = open("log.txt", O_WRONLY | O_CREAT, 0666);
    if(fd < 0)
    {
        perror("open");
        return 1;
    }
    close(fd);                                                                                     
    return 0;
}   
  • 我们设置权限为666,但是为什么最终显示文件权限是664呢?
  • 原因是我们Linux系统中设置了umask = 0002,umask会限制掉对应的权限,把Others的W权限去掉了
  • 如果我们想要创建权限为666的文件,则需要我们重设umask,系统调用函数umask
cpp 复制代码
int main()
{
		umask(0); 
    int fd = open("log.txt", O_WRONLY | O_CREAT, 0666);
    if(fd < 0)
    {
        perror("open");
        return 1;
    }
    close(fd);                                                                                     
    return 0;
}   

write函数

  • 上面open函数和close函数,我们已经实现了创建打开文件、关闭文件,现在我们在打开文件后进行写入操作
cpp 复制代码
int main()
{
		umask(0); 
    int fd = open("log.txt", O_WRONLY | O_CREAT, 0666);
    if(fd < 0)
    {
        perror("open");
        return 1;
    }
    const char* str = "1234567890\n";
    write(fd, str, strlen(str));
    close(fd);                                                                                     
    return 0;
}   
  • 当log.txt文件中已经保存了1234567890数据时,我们再次向该文件写入"abc",可以看到,使用write系统调用,并没有像C语言接口一样,先清空文件中的数据,再写入;而是从文件开始写入
  • 如果我们想要写入文件时先清空文件内容,再做写入,那么在打开文件时传入O_TRUNC:打开文件时清空文件内容
  • open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666)就类似于C语言接口的fopen("log.txt", "w")
  • 如果要使用追加方式写文件,open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666)中O_TRUNC(清空)改为O_APPEND,open("log.txt", O_WRONLY | O_CREAT | O_APPEND, 0666)
cpp 复制代码
int main()
{
		umask(0); 
		
    int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666); // 类似于fopen("log.txt", "w")
     //int fd = open("log.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);
    if(fd < 0)
    {
        perror("open");
        return 1;
    }
    const char* str = "1234567890\n";
    write(fd, str, strlen(str));
    close(fd);                                                                                     
    return 0;
}   
相关推荐
西电研梦2 小时前
26西电考研 | 寒假开始,机试 or C语言程序设计怎么准备?
c语言·考研·华为od·研究生·西安电子科技大学·计算机408
云小逸2 小时前
【Nmap 源码学习】Nmap 源码深度解析:nmap_main() 函数逐行详解
网络·windows·学习·nmap
执行部之龙2 小时前
TCP八股完结篇
网络·笔记·网络协议·tcp/ip
Zach_yuan2 小时前
从零理解 HTTP:协议原理、URL 结构与简易服务器实现
linux·服务器·网络协议·http
学嵌入式的小杨同学2 小时前
【嵌入式 GUI 实战】LVGL+MP3 播放器:从环境搭建到图形界面开发全指南
linux·c语言·开发语言·vscode·vim·音频·ux
爱吃rabbit的mq2 小时前
第13章:神经网络基础 - 感知机到多层网络
网络·人工智能·神经网络
晚风吹长发2 小时前
初步了解Linux中的POSIX信号量及环形队列的CP模型
linux·运维·服务器·数据结构·c++·算法
1+α2 小时前
汽车里的“神经网络”——CAN总线科普
c语言·stm32·嵌入式硬件·信息与通信
爱编码的小八嘎2 小时前
C语言对话-19.新的起点,第一部分
c语言