文章目录
- 1. 基础IO
- 1. 文件
- 2. 回顾C文件接口
- 2.1 hello.c写文件
- 2.2 hello.c读文件
- 2.3 接口介绍
- 3. open函数返回值
- 3.1 文件描述符fd
- 3.2 文件描述符的分配规则
- 3.2.1 代码1
- 3.2.2 代码2
- 3.2.3 重定向底层原理代码示例
- 3.2.4 使用 dup2 系统调用
- 3.3 缓冲区刷新问题
- 3.4 FILE
1. 基础IO
1. 文件
共识原理:
文件=内容+属性(文件被打开,必须先被加载到内存)
文件分为打开的文件和没打开的文件
打开的文件是谁打开的?
是进程打开的。
没打开的文件:我们最关注什么?在哪里存放呢?
在磁盘存放。
没有被存放的文件非常多,文件如何被分门别类的放置好?我们要快速的进行增删改查,快速找到文件。
文件被打开,必须先被加载到内存
进程:打开的文件 =
1:n
操作系统内部一定存在大量的被打开的文件。
操作系统要不要管理这些被打开的文件呢?
在内核中,一个被打开的文件都必须有自己的文件打开对象,包含文件的很多属性。struct XXX(文件属性:struct XXX *next);
2. 回顾C文件接口
2.1 hello.c写文件
#include <stdio.h>
#include <string.h>
int main() {FILE *fp = fopen("myfile", "w");if(!fp){printf("fopen error!\n");} const char *msg = "hello bit!\n";int count = 5;while(count--){fwrite(msg, strlen(msg), 1, fp);} fclose(fp); return 0;
}
2.2 hello.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;
}
当前路径是进程的当前路径
cwd
,如果我更改了当前进程的cwd
,就可以把文件新建到其他目录。
w
:写入之前,都会对文件进行清空处理。
w/a
:都是写入,w
清空并从头写,a
在文件结尾,追加写。
如果我们想要向显示器打印:
C语言程序
默认在启动的时候,会打开3个标准输入输出流(文件)
stdin
:键盘文件
stdout
:显示器文件
stderr
:显示器文件
文件实际上是在磁盘上的,磁盘是外部设备,访问磁盘文件实际上是访问硬件。
几乎所有的库只要访问硬件设备,必须要封装系统调用。
2.3 接口介绍
open
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
mode: 设置文件的访问权限(仅在创建文件时使用)
实例代码:
// 只读方式打开文件
int fd = open("file.txt", O_RDONLY);// 创建新文件,可读写
int fd = open("newfile.txt", O_RDWR | O_CREAT, 0644);
3. open函数返回值
在认识返回值之前,先来认识一下两个概念:
系统调用
和库函数
。上面的fopen fclose fread fwrite
都是C
标准库当中的函数,我们称之为库函数。而,
open close read write lseek
都属于系统提供的接口,称之为系统调用接口
所以,可以认为,f#
系列的函数,都是对系统调用的封装,方便二次开发。
3.1 文件描述符fd
通过对open函数的学习,我们知道了文件描述符就是一个小整数
0 & 1 & 2
Linux进程默认情况下会有3个缺省打开的文件描述符,分别是标准输入0, 标准输出1, 标准错误2.
0,1,2对应的物理设备一般是:键盘,显示器,显示器
所以输入输出还可以采用如下方式:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>int main()
{char buf[1024]; // 定义一个字符数组,用于存储从标准输入读取的数据,缓冲区大小为1024字节ssize_t s = read(0, buf, sizeof(buf)); // 调用read函数从文件描述符0(标准输入)读取数据// 读取的数据存储到buf中,最多读取sizeof(buf)字节,返回值为实际读取的字节数,类型为ssize_t(带符号的size_t)// 如果发生错误,返回-1;如果到达文件末尾,返回0if(s > 0){ // 检查read函数是否成功读取到数据,如果s > 0,表示成功读取了s字节的数据buf[s] = 0; // 在读取到的数据末尾添加一个空字符('\0'),将其转换为C字符串,这确保了后续使用字符串处理函数时的安全性write(1, buf, strlen(buf)); // 调用write函数将buf中的数据写入文件描述符1(标准输出)// 写入的数据长度为strlen(buf),即不包含末尾的空字符,返回值为实际写入的字节数write(2, buf, strlen(buf)); // 调用write函数将buf中的数据写入文件描述符2(标准错误)// 同样写入strlen(buf)字节的数据,这意味着程序会将输入的数据同时输出到标准输出和标准错误}return 0;
}
而现在知道,文件描述符就是从0开始的小整数。当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件。于是就有了file结构体。表示一个已经打开的文件对象。而进程执行open系统调用,所以必须让进程和文件关联起来。每个进程都有一个指针*files, 指向一张表files_struct,该表最重要的部分就是包涵一个指针数组,每个元素都是一个指向打开文件的指针!所以,本质上,文件描述符就是该数组的下标。所以,只要拿着文件描述符,就可以找到对应的文件
3.2 文件描述符的分配规则
3.2.1 代码1
#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
3.2.2 代码2
关闭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 可见,文件描述符的分配规则:在files_struct数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符。
3.2.3 重定向底层原理代码示例
代码:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>#define filename "log.txt" // 定义文件名常量int main(){close(1); // 关闭标准输出文件描述符(1)// 以创建、只写、清空方式打开文件,权限设置为666int fd = open(filename, O_CREAT|O_WRONLY|O_TRUNC, 0666);if(fd < 0){ // 打开文件失败则报错perror("open");return 1;}const char *msg = "hello linux\n"; // 定义要写入的字符串int cnt = 5; // 循环次数// 循环写入5次字符串到文件while(cnt){// 由于前面关闭了标准输出,此时文件描述符1会被重定向到新打开的文件write(1, msg, strlen(msg)); cnt--;}close(fd); // 关闭文件描述符return 0;
}
运行结果:
ydk_108@iZuf68hz06p6s2809gl3i1Z:~/108/lesson19$ ll
total 36
drwxrwxr-x 2 ydk_108 ydk_108 4096 Jan 22 16:41 ./
drwxrwxr-x 15 ydk_108 ydk_108 4096 Jan 22 16:39 ../
-rw-rw-r-- 1 ydk_108 ydk_108 0 Jan 22 16:39 log.txt
-rw-rw-r-- 1 ydk_108 ydk_108 64 Jan 22 16:40 makefile
-rwxrwxr-x 1 ydk_108 ydk_108 16872 Jan 22 16:41 mytest*
-rw-rw-r-- 1 ydk_108 ydk_108 881 Jan 22 16:41 mytest.c
ydk_108@iZuf68hz06p6s2809gl3i1Z:~/108/lesson19$ ./mytest
ydk_108@iZuf68hz06p6s2809gl3i1Z:~/108/lesson19$ cat log.txt
hello linux
hello linux
hello linux
hello linux
hello linux
ydk_108@iZuf68hz06p6s2809gl3i1Z:~/108/lesson19$
本来向显示器写入的文件,被写入到了文件里面,这个叫做输出重定向。
- 文件描述符基础知识:
Linux系统中,每个进程启动时默认打开3个文件描述符:
0:标准输入(stdin)
1:标准输出(stdout)
2:标准错误(stderr)
-
程序主要步骤:
close(1); // 关闭标准输出
- 首先关闭标准输出(文件描述符
1
) - 这会使文件描述符
1
空闲出来
int fd = open(filename, O_CREAT|O_WRONLY|O_TRUNC, 0666);
- 打开文件时使用的标志说明:
O_CREAT
:如果文件不存在则创建O_WRONLY
:以只写方式打开O_TRUNC
:如果文件存在则清空内容0666
:设置文件权限(读写权限)
- 由于前面关闭了文件描述符
1
,新打开的文件会使用最小可用的文件描述符,也就是1
- 首先关闭标准输出(文件描述符
-
写入操作:
write(1, msg, strlen(msg));
- 虽然代码中写的是向文件描述符
1
写入数据 - 但因为文件描述符
1
已经被重定向到了log.txt
文件 - 所以实际上是在向
log.txt
文件写入数据 - 循环
5
次,每次写入"hello linux\n"
- 虽然代码中写的是向文件描述符
-
程序执行效果:
- 会在当前目录创建
log.txt
文件 - 文件中会包含
5
行"hello linux"
- 如果之前存在同名文件,内容会被清空后重写
- 会在当前目录创建
一开始没有添加close(1);
:
后来添加了close(1);
后:
我们也可以和之前的结合起来理解:
3.2.4 使用 dup2 系统调用
函数原型:
int dup2(int oldfd, int newfd);
代码解释:
- 函数原型:
int dup2(int oldfd, int newfd);
- 参数含义:
oldfd
:旧的文件描述符(源)newfd
:新的文件描述符(目标)
- 功能说明:
- 将
newfd
指向oldfd
所指向的文件- 如果
newfd
已经打开,会先关闭它- 操作完成后,
oldfd
和newfd
指向同一个文件
- 返回值:
- 成功:返回新的文件描述符(即
newfd
的值)- 失败:返回
-1
,并设置errno
- 使用示例:
dup2(fd, 0);
#include <stdio.h> #include <unistd.h> #include <fcntl.h> #include <string.h>int main() {// 准备一个包含输入数据的文件const char *input_file = "input.txt";int fd = open(input_file, O_RDONLY);if(fd < 0) {perror("open");return 1;}// 将标准输入重定向到文件dup2(fd, 0);close(fd); // 关闭原文件描述符// 现在从标准输入读取实际上是从文件读取char buf[256];scanf("%s", buf); // 会从input.txt读取内容// input.txt的内容如果是:hello world// 则buf中只会读取到"hello",因为遇到空格就停止了读取printf("读取到的内容: %s\n", buf);return 0; }
dup2(fd, 1);
#include <unistd.h> #include <fcntl.h>int main() {// 打开一个文件int fd = open("output.txt", O_WRONLY|O_CREAT, 0644);// 将标准输出(1)重定向到fd指向的文件dup2(fd, 1);// 现在printf的输出会写入到output.txtprintf("Hello World\n");close(fd);return 0; }
dup2(fd, 2);
#include <stdio.h> #include <unistd.h> #include <fcntl.h> #include <string.h>int main() {// 打开错误日志文件const char *error_file = "error.log";int fd = open(error_file, O_WRONLY|O_CREAT|O_TRUNC, 0666);if(fd < 0) {perror("open");return 1;}// 将标准错误重定向到文件dup2(fd, 2);close(fd);// 以下错误信息会写入到error.log文件中fprintf(stderr, "这是一个错误信息\n");perror("测试错误");return 0; }
3.3 缓冲区刷新问题
在C语言的I/O系统中,有三种缓冲模式:
- 无缓冲(_IONBF):
- 数据直接发送到目的地,不进行缓冲
- 每次读写都直接与设备或文件交互
- 典型例子:stderr(标准错误)
- 适用场景:错误信息输出等需要立即显示的场合
示例:
#include <stdio.h>int main() {// 设置stdout为无缓冲setvbuf(stdout, NULL, _IONBF, 0);printf("这句话会立即输出");return 0;
}
- 行缓冲(_IOLBF):
- 遇到换行符’\n’时才进行实际的I/O操作
- 缓冲区满时也会进行I/O操作
- 典型例子:stdout(标准输出,当连接到终端时)
- 适用场景:终端输出等交互场合
示例:
#include <stdio.h>int main() {printf("这句话不会立即输出"); // 不会立即显示printf("这句话会立即输出\n"); // 因为有\n,会立即显示fflush(stdout); // 强制刷新缓冲区return 0;
}
- 全缓冲(_IOFBF):
- 缓冲区满时才进行实际的I/O操作
- 典型例子:文件操作
- 适用场景:文件读写等对实时性要求不高的场合
示例:
#include <stdio.h>int main() {// 打开文件设置为全缓冲FILE *fp = fopen("test.txt", "w");if(fp == NULL) return 1;// 设置缓冲区大小为1024字节char buf[1024];setvbuf(fp, buf, _IOFBF, sizeof(buf));fprintf(fp, "这些数据会先放在缓冲区");fflush(fp); // 强制写入文件fclose(fp);return 0;
}
行缓冲的缓冲区不是系统级别的缓冲区,行缓冲的缓冲区通常是由
C
语言标准库提供的。显示器的文件刷新方案是行刷新,所以在
printf
执行完遇到\n
的时候将数据进行刷新。用户刷新的本质,就是将数据通过
1+write
写入到内核中。
缓冲区刷新问题:
- 无缓冲:直接刷新
- 行缓冲:不刷新,直到遇到
\n
- 全缓冲:缓冲区满了才刷新
进程退出的时候,缓冲区也会刷新。
为什么要有这个缓冲区呢?
- 解决用户的效率问题
- 配合格式化
3.4 FILE
因为IO相关函数与系统调用接口对应,并且库函数封装系统调用,所以本质上,访问文件都是通过fd访问的。
所以C库当中的FILE结构体内部,必定封装了fd。
#include <stdio.h>
#include <string.h>int main()
{const char *msg0="hello printf\n";const char *msg1="hello fwrite\n";const char *msg2="hello write\n";printf("%s", msg0);fwrite(msg1, strlen(msg0), 1, stdout);write(1, msg2, strlen(msg2));fork();return 0;
}
运行出结果:
hello printf
hello fwrite
hello write
但如果对进程实现输出重定向呢? ./hello > file
, 我们发现结果变成了:
hello write
hello printf
hello fwrite
hello printf
hello fwrite
我们发现 printf
和 fwrite
(库函数)都输出了2
次,而 write
只输出了一次(系统调用)。为什么呢?
肯定和fork
有关!
一般
C
库函数写入文件时是全缓冲的,而写入显示器是行缓冲。
printf
和fwrite
是C
库函数,它们使用FILE
结构体的缓冲区,当发生重定向到普通文件时,数据的缓冲方式由行缓冲变成了全缓冲。
fork()
时会复制父进程的内存空间,包括FILE
结构体的缓冲区此时缓冲区中还存有
printf
和fwrite
写入但未刷新的数据而我们放在缓冲区中的数据,就不会被立即刷新,甚至
fork
之后父子进程各自都继承了这份缓冲区数据
但是进程退出之后,会统一刷新,写入文件当中。
当进程结束时,都会刷新各自的缓冲区,所以
printf
和fwrite
的内容出现两次
write
没有变化,说明没有所谓的缓冲。
为什么
write
只出现一次:
write
是系统调用,在fork()
之前就已经直接写入文件- 没有经过缓冲区,所以不存在缓冲区复制的问题
- 父子进程退出时也不会产生重复写入
综上: printf fwrite 库函数会自带缓冲区,而 write 系统调用没有带缓冲区。另外,我们这里所说的缓冲区,都是用户级缓冲区。其实为了提升整机性能,OS也会提供相关内核级缓冲区,不过不再我们讨论范围之内。 那这个缓冲区谁提供呢? printf fwrite 是库函数, write 是系统调用,库函数在系统调用的“上层”, 是对系统调用的“封装”,但是 write 没有缓冲区,而 printf fwrite 有,足以说明,该缓冲区是二次加上的,又因为是C,所以由C标准库提供。