基础IO
文章目录
C语言文件操作回顾
c语言写文件,先调用fopen库函数打开文件,获得文件指针fp,在通过fwrite函数把字符串中的内容写入到test文件当中。
#include<stdio.h>
#include<string.h>
int main() {
FILE* fp = fopen("test.txt", "w");
if (!fp) {
printf("fopen error!\n");
}
const char* str = "hello world!\n";
int count = 5;
while (count--) {
fwrite(str, strlen(str), 1, fp);
}
fclose(fp);
return 0;
}
c语言读文件
#include <stdio.h>
#include <string.h>
int main()
{
FILE* fp = fopen("myfile", "r");
if (!fp) {
printf("fopen error!\n");
}
char buf[1024];
const char* msg = "hello bit!\n";
while (1) {
//注意返回值和参数,此处有坑,仔细查看man手册关于该函数的说明
ssize_t s = fread(buf, 1, strlen(msg), fp);
if (s > 0) {
buf[s] = 0;
printf("%s", buf);
}
if (feof(fp)) {
break;
}
}
fclose(fp);
return 0;
}
C语言输入输出流
- stdin标准输入
- stdout标准输出
- stderr标准错误
系统调用I/O接口
写文件
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main() {
int fd = open("IO.txt", O_CREAT | O_WRONLY, 0664);
if (fd < 0) {
perror("open err!\n");
}
const char* str = "hello world!\n";
write(fd, str, strlen(str));
close(fd);
return 0;
}
读文件
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#define SIZE 128
int main() {
int fd = open("IO.txt", O_RDONLY);
if (fd < 0) {
perror("open err!\n");
}
char buf[SIZE];
//把打开的文件读到buf里
read(fd, buf, SIZE);
printf("fd:%d\nbuf:%s", fd, buf);
close(fd);
return 0;
}
open()
int open(const char *pathname, int flags, mode_t mode);
参数:
- pathname:要打开或创建的目标文件
- flags:打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行“或”运算,构成flags
这几个参数其实在底层是几个定义好的16进制数,而这几个数的二进制形式都有一个特点,就是二进制的比特位只有一个位为1,也就是说,每一个为1的比特位就能表示不同的参数形式,如果把这些参数用 | 运算,得到的数据,哪个位为1,就表示一种操作,就实现了一个参数传入好多种不同的操作方式。
- mode:打开文件的权限
如果open时使用了O_CREAT,就要对文件的属性进行设置
返回值:
- 成功的话返回新的文件描述符
- 失败返回-1
write()
ssize_t write(int fd, const void *buf, size_t count);
参数:
- fd:文件描述符
- buf:要写入的缓冲区的首地址
- conut:本次读取,期望写入多少个字节的数据
返回值:
实际写入的字节个数
read()
ssize_t read(int fd, void *buf, size_t count);
参数:
- fd:文件描述符
- buf:要读取的缓冲区的首地址
- conut:本次读取,期望读多少个字节的数据
返回值:
实际写入的字节个数
文件描述符
这个小小的整数有什么作用呢
将打开文件的open返回值都打印出来
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main(){
int fd1 = open("IO1.txt",O_RDONLY|O_CREAT);
int fd2 = open("IO2.txt",O_RDONLY|O_CREAT);
int fd3 = open("IO3.txt",O_RDONLY|O_CREAT);
int fd4 = open("IO4.txt",O_RDONLY);
int fd5 = open("IO5.txt",O_RDONLY);
printf("fd:%d\n",fd1);
printf("fd:%d\n",fd2);
printf("fd:%d\n",fd3);
printf("fd:%d\n",fd4);
printf("fd:%d\n",fd5);
close(fd1);
close(fd2);
close(fd3);
close(fd4);
close(fd5);
return 0;
}
可以看到open函数的返回值是从3开始依次增加的,如果没有文件,并且没有以O_CREAT的形式打开,那么就会打开失败,返回-1。
而这些成功返回值的数字,就叫做文件描述符,而我们看到返回值是从3开始的,那是因为0 1 2 号文件描述符是会被默认打开的
而0 1 2 号文件描述符分别对应标准输入,标准输出,标准错误。
文件描述符 | 对应文件 |
---|---|
0 | 标准输入 |
1 | 标准输出 |
2 | 标准错误 |
wirte函数传入1号文件描述符,可以看到原本要写入文件的字符串被输出到了显示器
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main() {
int fd = open("IO.txt", O_CREAT | O_WRONLY, 0644);
const char* str = "hello world!\n";
write(1, str, strlen(str));
return 0;
}
为什么一个int数字文件描述符就能对应不同的文件?
操作系统想要管理文件,要先对文件进行描述,把文件的属性信息用一个struct_file描述起来,struct_file中包括文件的名称,inode等信息,还有文件的内容。
遵循操作系统管理的原则"先描述,后组织",操作系统也要对描述的文件进行组织,用一个链表把文件结构体进行管理,打开文件就是在文件链表尾部加入文件结构体struct_file,而这些文件时被进程所打开的,也就是说,task_struct要对struct_file进行管理,打开Linux内核源码中的sched.h ,在task_struct结构体中可以找到这样一个结构体指针,它指向的是另一个结构体 files_struct 。
源码给出的解释,files_struct , /*open file information */ 打开文件信息,也就是说,通过这个结构体,进程控制块PCB就能打开文件,那么这个结构体中一定存放着文件结构体的位置。跳转到files_struct的定义:
里面存放着打开文件的相关属性,文件锁等,都是用来管理文件的,也就是管理file结构体。在末尾有一个数组fd_array[],这个数组是一个结构体指针数组,数组内存放的就是要管理的文件描述结构体也就是file结构体的指针,通过访问这个数组中的地址信息,进程就能对文件进行管理。数组的大小是一个宏定义,转到定义,我们发现这个宏的值是32,也就是通过这个数组,我们可以访问最多33个文件。
我们通过一个图片来看进程是怎么通过文件描述符来管理文件的:
我们所用的一些外设,键盘鼠标显示器等,他们的文件输入输出打开等的操作的实现源码肯定是不同的,比如进程默认打开的三个文件 0 1 2 号文件,他们的open write 等的方法肯定是不同的,而我们打开一个文件,也就是创建一个文件的file结构体,那么不同的设备对文件的操作也该存到这个结构体内部,但是C语言的结构体内只能存变量,不能存函数,所以在结构体file当中有一个file_operations(文件操作)结构体,用来指向硬件提供的一些底层的驱动代码,用来实现对不同外设的文件读写等的操作,这样一来一种结构体的不同的结构体对象就能保存不同外设的文件操作驱动代码。
打开不同的文件,就会调用不同的硬件操作驱动代码,这样在操作系统看来,结构体内的内容和方法都是一样的,也就是操作系统中的一切皆文件。
文件描述符的分配规则
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main() {
int fd = open("IO.txt", O_CREAT | O_WRONLY, 0644);
if (fd < 0) {
perror("open error!\n");
}
printf("fd:%d\n", fd);
close(fd);
return 0;
}
由于进程默认打开的进程占用了0 1 2 号文件描述符,所以我们再新打开一个文件,这个文件的文件描述符则从3开始
我们在进程开始的时候,手动关闭进程的2号文件
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main() {
//关闭二号文件描述符
close(2);
int fd = open("IO.txt", O_CREAT | O_WRONLY, 0644);
if (fd < 0) {
perror("open error!\n");
}
printf("fd:%d\n", fd);
close(fd);
return 0;
}
这时看到我们新打开的文件的文件描述符变成了2
- 文件描述符的分配规则:在fd_array数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符。
重定向
如果我们手动关闭1号文件,也就是标准输出文件,我们要输出到显示器上的内容还会不会显示?
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main() {
close(1);
int fd = open("IO.txt", O_CREAT | O_WRONLY, 0644);
if (fd < 0) {
perror("open error!\n");
}
printf("fd:%d\n", fd);
fflush(stdout);
close(fd);
return 0;
}
我们看到原本要显示到显示器上的“fd:1”被写入到了打开的文件当中,这就是重定向,常见的重定向有:>, >>, <
而不同的重定向的实现方法,只需要更改open文件时第二个参数就能实现
- 追加重定向
int fd = open("IO.txt", O_APPEND | O_WRONLY, 0644);
- 清空重定向
int fd = open("IO.txt",O_TRUNC|O_WRONLY,0644);
重定向的原理
关闭1号文件描述符,新打开的文件根据文件描述符分配规则就分配到了1号文件描述符,也就是说新打开的文件的文件描述符就变成了1,但是printf是一个库函数,默认是往1号文件打印,printf并不知道文件描述符对应的指针指向已经发生变化,依然会打印到1号文件描述符对应的文件,那么本该打印到屏幕上的数据就会被打印到1号文件描述符对应的文件中,也就是新打开的文件之中,完成重定向。
使用dup2系统调用完成重定向
使用man手册查看dup2
makes newfd be the copy of oldfd, closing newfd first if necessary
使newfd成为oldfd的拷贝,必要时先关闭 newfd
int dup2(int oldfd, int newfd);
重定向的原理是把新文件的文件描述符中指向的内容拷贝给1号文件描述符中指向的内容。
使newfd成为oldfd的拷贝也就是使1号文件描述符指向的内容成为新文件描述符指向内容的拷贝。
用dup2完成重定向:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main() {
int fd = open("IO.txt", O_WRONLY | O_APPEND);
printf("dup2 test");
//使1号文件描述符成为fd文件描述符的一份拷贝
dup2(fd, 1);
return 0;
}
FILE流
c语言的fopen,fwrite等文件操作函数的返回值是一个文件流的指针,也就是FILE* ,其实FILE是C库中的一个结构体,FILE结构体内部封装了fd文件描述符,所以说C语言的文件操作库封装了文件描述符来操作文件。
在/usr/include/stdio.h中有这样的定义
FILE其实是_IO_FILE的重命名
而stdin/stdout/stderr本质上是**_IO_FILE的指针**,也就是FILE的指针,也叫文件流指针。
再转到_IO_FILE的实现(/usr/include/libio.h)
这个结构体也就是FILE结构体内有一个int 类型的变量**_fileno**,这个就是文件描述符,FILE结构体通过文件描述符管理文件
缓冲区
使用一个系统调用函数write和c的两个库函数,分别往显示器打印一个字符串,并且在打印完成后进行fork创建子进程
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main() {
const char* str1 = "write\n";
const char* str2 = "printf\n";
const char* str3 = "fprintf\n";
//使用系统调用接口打印字符串
write(1, str1, strlen(str1));
//使用库函数打印字符串
printf(str2);
fprintf(stdout, str3);
//创建子进程
fork();
return 0;
}
三个字符串都被打印出来,我们对进程进行重定向,把本该打印到显示器的内存打印到空文件IO.txt中
我们发现,write(系统调用)被打印了一次,而printf和fprintf(库函数)被打印了两次。
其实这和缓冲区有关,往显示器打印和往文件打印所使用的缓冲方式是不一样的。分为行缓冲和全缓冲
- 行缓冲:遇到换行时,会立刻刷新
- 全缓冲:当缓冲区写满,或者进程退出时才会刷新
而重定向会影响进程的缓冲方式,原本的行缓冲变成了全缓冲,也就是说,本来遇到\n就该打印的字符串,缓冲方式变成了全缓冲,字符串被存到了缓冲区当中,没有立即打印,一直到了fork之后,子进程发生了写时拷贝,产生了两份数据,这两份数据在进程退出的时候都会被刷新出来。
但是,我们发现write却只刷新了一次,说明系统调用在输出的时候并不会把数据存到缓冲区,而是直接输出,说明系统调用接口没有所谓的缓冲区(用户级缓冲区),而printf和fprintf是库函数,却有缓冲区,说明缓冲区(用户级缓冲区)不是操作系统提供的,而是C语言标准库提供的。