Linux-6-基础IO

前言

Vue框架:Vue驾校-从项目学Vue-1
算法系列博客友链:神机百炼

C语言文件操作函数:

一表总结:

  • C语言属于用户层,只能通过文件指针FILE*来访问和操作文件
函数 功能 适用流
fgetc 字符输入函数 所有输入流
fputc 字符输出函数 所有输出流
fgets 文本行输入函数 所有输入流
fputs 文本行输出函数 所有输出流
scanf
fscanf 格式化输入函数 所有输入流
printf
fprintf 格式化输出函数 所有输出流
fread 二进制输入 文件
fwrite 二进制输出 文件
sscanf 将其他类型数据,转化为格式化数据
sprintf 将格式化数据,转化为其他类型数据

文件打开关闭:

fopen():

  • 作用:打开指定路径下的文件,返回对该文件的指针
  • 参数:
    1. 目标文件路径
    2. 宏定义好的打开方式mode
  • 返回值:
    1. 成功:新打开文件的指针
    2. 失败:NULL
  • 打开方式:通过携带不同参数mode,实现创建编辑/追加编辑/只读
#include <stdio.h>
FILE *fopen(const char *path, const char *mode);

/*
r:只读已存在的文件
r+:读写已存在的文件
rb:只读已存在的二进制文件
rb+:读写已存在的二进制文件

w:只写,创建/覆盖文件
w+:可读可写,创建/覆盖文件
wb:只写,创建/覆盖二进制文件
wb+:可读可写,创建/覆盖二进制文件

a:只写,创建/追加文件
a+:可读可写,创建/追加文件
ab:只写,创建/追加二进制文件
ab+:可读可写,创建/追加二进制文件
*/
  • 实例:
#include <stdio.h>
int main(){
 	FILE *fp = fopen("文件主干名.文件后缀","打开方式");
 	
	if(fp == NULL){
	    perror("fopen失败");
	    return -1;
	}
	
	fclose(fp);
	fp = NULL;				//关闭文件后记得将指针指向NULL
 	return 0;
}

fclose():

  • 作用:关闭文件指针所指向的文件
  • 参数:FILE*文件指针
  • 实例:
#include <stdio.h>
int main(){
	FILE* fp = fopen("文件路径",mode);
	
	if(fp == NULL){
		perror("fopen失败");
		return -1;
	}

	fclose(fp);
	fp = NULL;

	return 0;
}

三大标准文件指针:

标准输入流:

  • stdin:对应设备为键盘

标准输出流:

  • stdout:对应设备为显示器

标准错误流:

  • stderr:对应设备为显示器

文件写入:

fputs():

  • 作用:向指定文件输入内容
  • 适用情况:向文件写入字符串
  • 参数:输入内容的字符串指针 & 指定文件的文件指针
  • 实例:
#include <stdio.h>
int main(){
	FILE* fp = fopen("文件路径",mode);
	if(fp == NULL){
		perror("fopen失败");
		return -1;
	}
	fputs("hello fputs\n", stdout);
	fputs("hello fputs\n", fp);
	fclose(fp);
	fp = NULL;
	return 0;
}

fprintf():

  • 作用:按照指定格式将内容输入到目标文件中
  • 适用情况:多用于将结构体内字段输入到文件中
  • 参数:文件指针,格式,具体内容
  • 实例:
#include <stdio.h>
struct test{
	char a;
	int b;
	double c;
};
int main(){
	struct test t = {'1', 2, 3};
	FILE* fp = fopen("文件路径",mode);
	if(!fp){
		perror("fopen失败");
		return -1;
	}
	fprintf(fp,"%c %d %lf",t.a, t.b, t.c);
	fclose(fp);
	fp = NULL;
	return 0;
}

fwrite():

  • 作用:从某地址开始,将指定大小字节数据写入目标文件
  • 适用情况:文件传输
  • 参数:内容地址,每个元素大小,元素个数,文件指针
  • 返回值:比较写入元素个数和目标元素个数
    1. 写入成功完成:元素个数
    2. 写入失败:报错
  • 实例:
#include <stdio.h>
#include <string.h>
int main(){
    FILE *fp = fopen("路径","打开方式");
    if(!fp){							
		perror("文件打开失败\n");
		return -1;
	}
	
	const char *msg = "fwrite()用法示范";
    for(int i=0; i<5; i++){
		fwrite(msg, 1, strlen(msg), fp);
    }
    
    fclose(fp);
    fp = NULL;
    return 0;	//exit()自带流关闭
}

文件读取:

fgets():

  • 作用:读取文件中指定字节数内容
  • 参数:结果保存的位置,读取的字节数,文件指针
  • 实例:
#include <stdio.h>
int main(){
	FILE* fp = fopen("文件路径",mode);
	if(fp == NULL){
		perror("fopen失败");
		return -1;
	}
	char arr[20];
	fgets(arr, 20, fp);	//从文件中读取20个字符
	printf("%s\n", arr);

	fgets(arr, 20, stdin);	//从键盘读取20个字符
	printf("%s\n", arr);

	fclose(fp);
	pf = NULL;
	return 0;
}

fscanf():

  • 作用:按指定格式从文件中读取数据
  • 适用情况:从文件中读取内容写入结构体变量中
  • 参数:文件指针,输入格式,存储地址
  • 实例:
#include <stdio.h>
struct test{
	char a;
	int b;
	double c;
};
int main(){
	struct test t;
	FILE* fp = fopen("文件路径",mode);
	if(fp == NULL){
		perror("fopen失败");
		return -1;
	}
	fscanf(fp, "%c %d %lf", &t.a, &t.b, &t.c);

	fclose(fp);
	fp = NULL;
	return 0;
}

fread():

  • 作用:读取文件中指定字节数的数据
  • 适用情况:文件传输
  • 参数:存储地址&每次读取字节数&读取次数&文件指针
  • 返回值:当次读取的字节数
  • 搭配函数:feof(FILE* fp)查看当前文件是否读取完毕
  • 实例:
#include <stdio.h>
#include <string.h>
int main(){
	FILE* fp = fopen("文件路径",mode);
	if(!fp){
		perror("fopen失败");
		return -1;
	}
	
	char buffer[1024];
	while(1){
		ssize_t s = fread(buffer, 1, 24, fp);
		if(s > 0){
			buffer[s] = 0;
			printf("%s\n", buffer);
		}
		if(feof(fp)) break;
	}
	
	fclose(fp);
	return 0;
}

进程files_struct:

作用:

  • 问题:由于进程独立性,每个进程打开的文件不同,所以每个进程需要专门创建一个结构来维护其所调用的文件
  • 存储形式:files_struct单独开辟内存空间,由PCB/task_struct中的file*指针来连接
  • fd:文件描述符,本质是file*[]数组的下标
  • fd_array:文件描述符表,本质是文件指针数组
  • 图示:

    files_struct

  • 每个进程的task_struct中*file所指向的files_struct中fd_array数组中0 1 2三个元素都是默认的:
    1. fd_array[0]:标准输入,对应硬件是键盘
    2. fd_array[1]:标准输出,对应硬件是显示器
    3. fd_array[2]:标准错误,对应硬件是显示器

== printf()不可以视作是fprintf()固定stdout参数,实时上printf()函数输出对象是fd_array[1] ==

字符文件:

  • 含义:linux下七大文件类型中的字符设备文件,其余六大为普通文件/目录文件/块文件/套接字文件/管道文件/链接文件
  • 原因:这些文件中保存的都是char型数据
    1. scanf()将char通过匹配转化为int double…
    2. printf()将int double…转化为char
  • 举例:
    1. 键盘驱动文件,标准输入流stdin
    2. 显示器驱动文件,标准输出流stdout,标准错误流stderr

文件操作系统调用接口:

含义:

  • 系统调用接口比C语言库函数更加底层:

    计算机层次

接口应用:

  • 系统调用接口比C语言文件操作函数更底层,直接体现在系统调用接口的操作对象是进程中文件描述符表的fd,而C语言文件操作函数的操作对象是文件指针

open():

用法:
  • 作用:打开指定路径下文件,将文件指针装填进入进程fd_array[]中
  • 参数:文件路径,打开方式,创建权限
  • 返回值:
    1. 打开成功:当前文件指针在文件描述符表中的下标
    2. 打开失败:-1
  • 函数说明:
//创建文件时需要赋予权限
int open(const char *pathname, int flags, mode_t mode);
//pathname:路径名
/*flag:打开模式
	O_RDONLY: 只读打开
 	O_WRONLY: 只写打开
 	O_RDWR : 读,写打开
 	这三个常量,必须指定一个且只能指定一个
 	O_CREAT : 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限
 	O_APPEND: 追加写
 	这三个变量,可以不指定,但是必须搭配前三个使用
*/
//mode:要赋予该文件的权限,还要在代码中搭配unmask(减去的权限)使用

//打开已有文件时不需要赋予权限
int open(const char *pathname, int flags);
  • 实例:
#include <stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
int main(){
	umask(0);				//不做权限删除
	int fd = open("myfile", O_WRONLY|O_CREAT, 0644);	//当前文件夹下创建后写入
	if(fd < 0){
		 perror("open失败");
		 return 1;
 	}
 	close(fd);
 	return 0;
}
内存角度理解:
  1. 将指定路径下的文件及其相关信息结合为一个file结构体,存储在内存中
  2. 在进程file*所指向的files_struct的fd_array[]中加入该file结构体的文件指针
与fopen()区别:
  1. fopen()是用户层,对系统调用接口open()做了进一步封装
  2. fopen()返回值为文件指针,open()返回值为fd
  3. open()打开文件,但是不维护文件读写缓冲区。fopen()打开文件,且维护对应读写缓冲区

close():

用法:
  • 参数:文件描述符
  • 作用:关闭进程对应fd_array[]中指定的fd项对应文件
  • 实例:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl>
int main(){
	umask(0);				//不做权限删除
	int fd = open("myfile", O_WRONLY|O_CREAT, 0644);	//当前文件夹下创建后写入
	if(fd < 0){
		 perror("open失败");
		 return 1;
 	}
 	close(fd);
 	return 0;
}
与fclose()区别:
  1. fclose()是用户层,对系统调用接口close()进行了封装
  2. close()关闭文件但不维护文件读写缓冲区,fclose()关闭文件且维护文件读写缓冲区

write():

  • 作用:向fd_array[]中指定fd对应文件中写入内容

  • 参数:fd,内容字节数,内容指针

  • 返回值:本次写入数据的字节数

  • 举例:

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(){
	umask(0);
	int fd = open("myfile", O_WRONLY|O_CREAT, 0644);
	if(fd < 0){
		 perror("open");
		 return 1;
 	}
 	
 	const char *msg = "hello world!\n";
 	for(int i=0; i<5; i++){
 		write(fd, msg, strlen(msg));
        //fd:被读文件描述符。msg:要写入内容首地址。strlen():每次读取的字节数
 	}
 	
 	close(fd);
 	return 0;
}

read():

  • 作用:读取fd_array[]中指定fd对应文件中的内容
  • 参数:fd,内容存储地址,内容大小
  • 返回值:
    1. 未读取完:本次读取内容的字节数
    2. 读取完成:0
  • 举例:
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(){
	int fd = open("myfile", O_RDONLY);					
	if(fd < 0){
		perror("open");
		return 1;
	}
	
 	const char *msg = "hello world!\n";
 	char buf[1024];
 	while(1){
 		ssize_t s = read(fd, buf, strlen(msg));
 		if(s > 0){
 			printf("%s", buf);
 		}else{
 			break;
 		}
 	}
 	
 	close(fd);
 	return 0;
}

重定向:

fd分配规则:

最小分配:

	1. 背景:每个进程的files_struct中的fd_array[]前三者已经被填充完:标准输入流 / 标准输出流 / 标准错误流
	2. 分配规则:进程每通过open()打开一个文件时,选取fd_array[]中**没有对应file且下标最小的值作为访问该文件的下标**

实例证明:

  • 直接打开一个文件,其fd值为3:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(){
	int fd = open("文件路径",O_RDONLY);
	if(fd<0){
		perror("open");
		return 1;
	}
	printf("fd: %d\n", fd);
	close(fd);
	return 0;
}
  • 关闭掉stdin/stdout/stderr,新打开的文件fd为0/1/2:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(){
 	close(0);
 	int fd = open("myfile", O_RDONLY);
 	if(fd < 0){
 		perror("open");
 		return 1;
 	}
 	printf("fd: %d\n", fd);
 	close(fd);
 	return 0;
}

重定向的含义:

  • 前提:

    1. 进程通过task_struct中file*指针所指向的files_struct中的文件指针数组fd_array[fd]寻找对应文件
    2. 每个进程所创建的file_struct中fd_array数组前三元素默认是 输入流stdin 输出流stdout 错误流stderr
    3. printf()函数输出对象始终是fd_array[1]
  • 重定向:

    1. 含义:将fd_array[]中下标fd和具体文件的对应关系改变
    2. 举例:fd_array[0]原本指向键盘,现在指向具体路径下的某个文件
    3. 实现:
      1. dup2()函数实现重定向
      2. 利用文件描述符fd最小分配机制,close()前面的fd_array[]后实现重定向

close()实现重定向:

  • 利用fd的最小分配机制:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(){
 close(1);			//为fd_array[1]重定向
 int fd = open("当前路径下的文件可以直接用文件名", O_WRONLY|O_CREAT, 00644);
 if(fd < 0){
 	perror("open");
 	return 1;
 }
 printf("fd: %d\n", fd);	//输出为1
 fflush(stdout);			//刷新缓冲区
 close(fd);
 exit(0);
}

dup2()实现重定向:

  • dup2()函数作用:
    将fd_array[newfd]和fd_array[oldfd]都指向fd_array[oldfd]所指向的文件
#include <unistd.h>
int dup2(int oldfd, int newfd);
  • 实例:通过修改fd_array[1]让printf()实现write()
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
int main(){
  int fd = open("log.txt", O_RDWR|O_CREAT, 00644);
  if(fd < 0) {
    perror("open失败");
    return -1;
  }
  close(1);
  dup2(fd,1);
  while(1){
    char msg[1024];
    ssize_t len =  read(0, msg, sizeof(msg)-1);
    if(len < 0){
      perror("read结束");
      break;
    }
    printf("%s", msg);
    fflush(stdout);
  }
  close(fd);
  return 0;
}
  • 编译运行后查看此时新建的log.txt内容:

    dup2实现printf()的write()

缓冲区:

分类:

  • 文件由进程打开,不同进程打开同一文件而缓冲区不同,同一进程打开不同文件而缓冲区亦不同,初步说明缓冲区其实是属于进程地址空间布局内
  • 用户区的缓冲区:
    程序员可操控,如fflush(stdout)
  • 系统内核区的缓冲区:
    程序员不可操控,由OS适时刷新
  • 两者关系:

    缓冲区关系

建立:

  • 执行open(“路径”,打开方式,权限) / open(“路径”,打开方式)时:
  1. 对应文件内容和文件相关信息结合产生file结构体
  2. 进程对应fd_array内新增file*指针
  3. 进程地址空间内新增文件读写缓冲区

缓冲方式:

  • 无缓冲:所有输入直接输出到指定文件

  • 行缓冲:

    1. 适用情况:打印到屏幕
    2. 刷新时刻:遇到\n 或 进程return
  • 全缓冲:

    1. 适用情况:写入到文件
    2. 刷新时刻:写入完毕 / fflush(stdout) / 进程return

缓冲区维护:

  • 写入方式:
  1. printf()是C语言库函数
  2. fprintf()是C语言库函数
  3. write()是系统调用接口
  • 写一段证明代码,探究文件读写缓冲区是由OS还是用户层的C语言维护:
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(){
	printf("C语言printf函数\n");
	fprintf(stdout, "%s\n", "C语言fprintf函数");
	char* msg = "系统调用write接口\n";
	write(1, msg, strlen(msg));
	fork();
	return 0;
}
  • 分别采用打印行刷新和文件写入全刷新的方式进行打印:

    刷新


    发现行刷新时虽然有子进程,但是只打印了一次;全刷新时虽然有子进程,但是只有C语言函数写入了两次
  • 分析父子进程的写时拷贝:
  1. 写时拷贝前:

    写时拷贝前

  2. 写时拷贝后:

    写时拷贝后

  3. 过程分析:

    1. 父进程和子进程初始享有的程序/数据/files_struct/文件缓冲区一致
    2. 向屏幕打印时,printf() & fprintf()写入缓冲区后遇到\n,缓冲区内数据清空,转移到stdout内;子进程运行到return时需要清空缓冲区,关闭输入输出流,不论是否写时拷贝,此时缓冲区已经为空
    3. 向文件输出时,printf() & fprintf()写入缓冲区后等待手动fflush()或自动exit() return,此时缓冲区内保留数据,子进程运行到return时需要清空缓冲区,发生写时拷贝,子进程创建出了自己的缓冲区,且其中数据和父进程一致,父子进程return前清空各自缓冲区,关闭各自输入输出流
    4. 系统调用接口write()内数据未经过缓冲区,直接写入了文件中,说明缓冲区是由用户层的C语言创建和维护使用的

相关文章

学习编程是顺着互联网的发展潮流,是一件好事。新手如何学习...
IT行业是什么工作做什么?IT行业的工作有:产品策划类、页面...
女生学Java好就业吗?女生适合学Java编程吗?目前有不少女生...
Can’t connect to local MySQL server through socket \'/v...
oracle基本命令 一、登录操作 1.管理员登录 # 管理员登录 ...
一、背景 因为项目中需要通北京网络,所以需要连vpn,但是服...