前言
C语言文件接口
C 语言读写文件
1.C语言写入文件
2.C语言读取文件
stdin/stdout/stderr
系统文件IO
文件描述符fd:
文件描述符分配规则:
文件描述符fd:
前言
我们早在C语言中学习关于如何用代码来管理文件,比如文件的输入和文件的输出,一些文件的接口,如何深入学习文件的知识,在Linux下一切皆文件,今天我们探讨Linux的基础I/O。
1>文件=内容+属性
2>访问文件之前,都需要先打开文件,并且对文件的修改都是通过执行代码的方式完成的,文件必须加载到内存中
文件被访问被修改必须得在内存中完成,因为CPU只能访问内存,所以文件必须加载到内存中。 打开文件->文件被加载到内存中
3>文件由谁打开? -> 由进程打开文件
4> 一个进程可以打开多个文件,由操作系统管理多个被打开的文件,那么这些文件是怎样被管理的: 先描述,再组织,内核中一定要有描述被打开文件的结构体,使用其定义对象
5>系统中不是所有的文件都被进程打开了, 没有被打开的文件就在磁盘中
C语言文件接口
C 语言读写文件
🗡文件操作:
首先要打开文件:打开成功,返回文件指针;打开失败,返回NULL
最后要关闭文件
🗡代码操作:
FILE *fopen(const char *path, const char *mode);
int fclose(FILE *fp);
fwrite | size_t fwrite ( const void * ptr, size_t size, size_t count, FILE * stream ) |
fclose | int fclose ( FILE * stream ); |
1.C语言写入文件
int fputs(const char *s, FILE *stream);
int fprintf(FILE *stream, const char *format, ...);
1. 如下,我们以"w"的方式打开文件,以"w"方式打开文件会先清空文件的内容然后再向文件写入内容。
#include <stdio.h>
int main()
{
FILE *fp=fopen("./log.txt","w");
if(fp==NULL)
{
perror("fopen");
return 1;
}
fclose(fp);
return 0;
}
2. 我们使用"a"方式(append) 打开文件 "a"方式 是向文件的末尾追加内容
#include <stdio.h>
#include <unistd.h>
#include <string.h>
int main()
{
FILE *fp = fopen("log.txt","a");
if(fp == NULL)
{
perror("fopen");
return 1;
}
const char* s = "hello linux\n";
fwrite(s,strlen(s),1,fp);
return 0;
}
2.C语言读取文件
char *fgets(char *s, int size, FILE *stream);
int fscanf(FILE *stream, const char *format, ...);
#include <stdio.h>
#include <unistd.h>
#include <string.h>
int main()
{
FILE *fp = fopen("./log.txt","r");
if(fp == NULL)
{
perror("fopen");
return 1;
}
char buffer[64];
while(fgets(buffer,sizeof(buffer),fp))
{
printf("%s",buffer);
}
return 0;
}
stdin/stdout/stderr
C默认会打开三个输入输出流,分别是
stdin 标准输入 键盘设备
stdout 标准输出 显示器设备
stderr 标准错误 显示器设备
仔细观察发现,这三个流的类型都是FILE*, fopen返回值类型,文件指针
stdin、stdout、stderr 都可以直接使用,例如:
系统文件IO
上面的 fopen fclose fread fwrite 都是C标准库当中的函数,我们称之为库函数(libc)。而, open close read write lseek 都属于系统提供的接口,称之为系统调用接口
访问文件不仅仅要有C语言上的文件接口,OS必须提供对应的访问文件的系统调用
man open查看
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
参数:
pathname: 要打开或创建的目标文件
flags: 打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行“或”运算,构成flags。参数: O_RDONLY: 只读打开O_WRONLY: 只写打开O_RDWR : 读,写打开 这三个常量,必须指定一个且只能指定一个O_CREAT : 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限O_APPEND: 追加写
返回值:
返回值:
成功:新打开的文件描述符失败:-1
来看下面的例子
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main()
{
int fd = open("./log.txt",O_WRONLY | O_CREAT);
if(fd < 0)
{
printf("open error\n");
// return 1;
}
close(fd);
return 0;
}
此时我们可以观察到 创建出来的文件的权限是乱的
这是因为,没有这个文件,要创建它,系统层面就必须指定权限是多少!我们采用权限设置的八进制方案
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main()
{
int fd = open("./log.txt",O_WRONLY | O_CREAT,0644);
if(fd < 0)
{
printf("open error\n");
return 1;
}
close(fd);
return 0;
}
此时的权限就正常了。
其中我们发现,我们传入的flag为 O_WRONLY|O_CREAT,中间为什么要用|连接起来呢:
这是一种用户层给内核传递标志位的常用做法。int有32个bit位,一个bit代表一个标志,就可以传递多个标志位且位运算效率较高。这些O_RDONLY、O_WRONLY、O_RDWR 都是只有一个比特位是1的数据,并且相互不重复,这样 |在一起,就能传递多个标志位。
看看下面这个例子
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#define ONE 1
#define TWO (1<<1)
#define THREE (1<<2)
#define FOUR (1<<3)
#define FIVE (1<<4)
void Print(int flag)
{
if(flag & ONE ) printf("1\n");
if(flag & TWO) printf("2\n");
if(flag & THREE) printf("3\n");
if(flag & FOUR) printf("4\n");
if(flag & FIVE) printf("5\n");
}
int main()
{
Print(ONE);
printf("------------\n");
Print(TWO);
printf("------------\n");
Print(ONE|TWO);
printf("------------\n");
Print(THREE|FOUR|FIVE);
printf("------------\n");
Print(ONE|TWO|THREE|FOUR|FIVE);
printf("------------\n");
return 0;
}
文件描述符fd:
open函数的返回值是就是文件描述符,类型为int,下面我们来看看fd的值
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{
int fd1 = open("./log1.txt",O_WRONLY | O_CREAT, 0644);
int fd2 = open("./log2.txt",O_WRONLY | O_CREAT, 0644);
int fd3 = open("./log3.txt",O_WRONLY | O_CREAT, 0644);
int fd4 = open("./log4.txt",O_WRONLY | O_CREAT, 0644);
printf("fd:%d\n",fd1);
printf("fd:%d\n",fd2);
printf("fd:%d\n",fd3);
printf("fd:%d\n",fd4);
close(fd1);
close(fd2);
close(fd3);
close(fd4);
return 0;
}
我们可以看到fd的值是从3开始的,一次打印出了3、4、5、6 那么前面的0,1,2去了哪里?
这时候我们想到了stdin,stdout,strerr ,当我们的程序运行起来变成进程,默认情况下,OS会帮助我们打开三个标准输入输出,012其实分别对应的就是标准输入stdin、标准输出stdout、标准错误stderr。对应硬件设备也是键盘、显示器、显示器。
0代表标准输入
1代表标准输出
2代表标准错误
#include <stdio.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;
}
printf("fd: %d\n", fd);
close(fd);
return 0
}
此时输出结果为 fd:3
再关闭0或者2
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
close(0);
//close(2);
int fd = open("myfile", O_RDONLY);
if(fd < 0){
perror("open");
return 1;
}
printf("fd: %d\n", fd);
close(fd);
return 0;
}
得到的结果会是fd:0 或者fd:2
文件描述符分配规则:
这样文件描述符被分配为01234678… 这样从0开始,连续的整数。 并且会优先分配 最小的,未被使用过的。每次给新文件分配的fd
,是从fd_array[]中找一个最小的、未被使用的作为新的fd。
所有的文件操作都是进程执行对应的函数,即本质上是进程对文件的操作。
如果一个文件没有被打开,这个文件是在磁盘上。如果我创建一个空文件,该文件也是要占用磁盘空间的,因为文件的属性早就存在了(包括名称、时间、类型、大小、权限、用户名所属组等等),属性也是数据,所谓“空文件”是指文件内容为空。
即磁盘文件 = 文件内容 + 文件属性。事实上,我们之前所学的所有文件操作都可以分为两类:对文件内容的操作 + 对文件属性的操作(fseek、ftell、rewind、chmod、chgrp等等).
要操作文件,必须打开文件(C语言fopen、C++打开流、系统上open),本质上,就是文件相关的属性信息从磁盘加载到内存。
操作系统中存在大量进程,进程可以打开多个文件,即进程 : 文件 = 1 : n ,系统中可能存在着更多的打开的文件(暂时不考虑一个文件被多个进程打开的特殊情况)。那么,OS要不要把打开的文件在内存中(系统中)管理起来呢?那么就要上管理的六字真言:先描述,再组织!
打开的这么多文件,怎么知道哪些是我们进程的呢?操作系统为了让进程和文件之间产生关联,进程在内核创建struct files_struct 的结构,这个结构包含了一个数组 struct file* fd_array[] ,也就是一个指针数组,把表述文件的结构体地址填入到特定下标中。
文件描述符fd:
此时我们cat命令查看log.txt文件,内容为空
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<string.h>
int main()
{
close(1);
int fd=open("log.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);
printf("fd:%d\n",fd);
return 0;
}
我们这段代码旨在打开log.txt文件,并且向显示器上打印fd的值。
但是我们执行程序之后,显示器上并没有出现我们期望的fd的值
反而我们cat 一下log.txt,发现fd的值竟然打印在了log.txt文件中
首先分析一下fd的值, 我们关闭了"1" 此时1 就是最小的且未被使用的,所以此时open的返回值是1;
对于本应打印在显示器上的值打印在文件中这件事情,printf底层封装了一些write,stdout等
此时传给printf的fd为1,那么将 文件描述符1 传递给进程后,进程就开始向log.txt文件中打印信息
同时我们也知道了 printf底层是在向标准输出(stdout)打印
int fprintf(FILE *stream, const char *format, ...);
stdout -> FIEL{fileno = 1} -> log.txt// stdout只认识1,只对1输入输出
extern : dup2
#include <unistd.h>
int dup2(int oldfd, int newfd); //oldfd->newfd
dup2() makes newfd be the copy of oldfd, closing newfd first if necessary, but note the following:
* If oldfd is not a valid file descriptor, then the call fails, and newfd is not closed.
* If oldfd is a valid file descriptor, and newfd has the same value as oldfd, then dup2() does nothing, and returns newfd.
拷贝的是fd
对应内容,最终相当于全部变成old.