关于文件的一些概念
- 文件包含文件内容和文件属性
- 对文件的操作包括对文件内容的操作和对文件属性的操作,也有可能同时进行对文件内容和文件属性的操作
- 打开文件这个动作是指的将文件的属性和内容加载到内存中
- 磁盘上并不是所有的文件都会被加载到内存中
- 对文件的理解可以分为内存文件和磁盘文件
- 通常当我们在对文件进行操作(打开文件,访问文件....)时,实质上是程序加载到内存中变成的进程对文件进行操作
- 这里讨论的就是进程和“打开文件”的关系
C语言中的文件操作函数实际上是对系统接口的封装,在向文件写入数据时是对磁盘内进行写入,只有操作系统有资格向硬件磁盘写入。上层(用户)如何调用操作系统呢?要通过相应的系统接口!平时我们没有用到系统接口是因为C语言的函数都经过了封装。
为什么要进行封装?因为如果在程序中直接进行系统调用(linux环境),将代码放入到windows环境下就不会兼容。不具有跨平台特性。
为了更好地了解底层原理,所以我们有必要学一学文件级别的系统接口
1.系统接口
open

返回值小于0,打开失败。
flags代表打开方式,就像C语言中的r,w等等。不过这里要特殊一些。
O_RDONLY(只读), O_WRONLY(只写), O_RDWR(读写),O_APPEND(读写),O_CREAT(没有文件创建文件),O_TRUNC(如果原来文件中有内容,将内容全部清空)等,他们实际上都是宏,是系统传递表计位,是用位图结构进行传递的,每一个宏标记,一般只需要一个比特位是1,并且和其他的宏对应的标记不能相同如下例。所以他们也可以是或的形式。
mode代表文件初始权限。如果被打开的文件是第一次出现,那么要给上初始权限。否则文件的显示权限部分会出现乱码(不等于最终权限)
int fd = open("text.txt",O_RDONLY)//只读
int fd = open("text.txt",O_WRONLY)//只写
int fd = open("text.txt",O_RDWR)//读写
int fd = open("text.txt",O_RDONLY|O_CREAT)//只读,如果没有文件创建文件
int fd = open("text.txt",O_RDONLY|O_WRONLY|O_CREAT)//读写,如果没有文件创建文件
int fd = open("text.txt",O_RDONLY|O_CREAT,0777);//初始权限给的是0777
这种程序设计方法非常像:利用位运算来达到选择特定方法,十分的优雅。


看一个例子,他会向文件log打印str,用vim打开log文件,看看写进去了没有


如果是strlen(str)+1呢?用vim打开后,发现'\0'被识别成了其他符号,说明linux是会将C语言中'\0'识别成^@的!


open函数最后有一个权限参数。权限是什么意思?是给这个文件权限的初值!在文件权限那一节有详细讲过,但是给的值并不一定在操作系统就是这个权限,因为要和操作系统的umask进行运算,最终形成权限,即使我们给log的是0666,但是,实际权限却是0664,
如果非要0666呢!?之前说过可以在操作系统更改umask,今天来说一个可以在代码中修改umask,并且只对当前进程有效:
![]()

要先将log去除,在调用函数才能创建新的log,为其赋权限。

接下来说一说C语言的文件调用:

write

#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<stdio.h>
#include<unistd.h>
#include<string.h>
int main()
{
int fd = open("log.txt",O_RDWR|O_CREAT,0666);
if(fd<0)
{
perror("open");
return 1;
}
const char* str = "hello world\n";
write(fd,str,strlen(str));
printf("%d\n",fd);
close(fd);
return 0;
}
//这里可以把字符串中的'\0'作为结束标志写入到文件中吗?答案是否定的,因为只有在C语言中
//'\0'是字符串结束的标志,在其他的打开方式下,'\0'可能会被识别成其他的字符!
write(fd, buf, count)尝试从内存地址buf处向文件描述符fd写入count个字节的数据。返回值说明:
返回值 ≥ 0:表示实际写入的字节数,可能小于
count。返回值 = -1:表示出错,并设置
errno。调用者必须保证
buf指向的内存至少有count个字节可读,count应该小于等于你实际准备好的数据长度。
read

#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<stdio.h>
#include<unistd.h>
#include<string.h>
int main()
{
int fd = open("log.txt",O_RDWR|O_CREAT,0666);
if(fd<0)
{
perror("open");
return 1;
}
char str[128];
ssize_t s= read(fd,str,127);
if(s>0)
{
str[s] = '\0';
printf("%s",str);
}
close(fd);
return 0;
}
2.文件描述符
open函数的返回值fd就是文件描述符。每次打开函数后,fd都是从3开始的,那么0,1,2呢?

![]()
这里可以直接说明:0是标准输入(键盘),1是标准输出(显示器),2是标准错误(显示器),每次都是默认打开的。不禁联想到C语言中的FILE* stdin,FILE* stdout,FILE* stderr。

验证:

linux下一切皆文件
被打开的文件在内存中,进程也在内存中。所以系统在运行过程中,会存在大量被打开的文件。操作系统要对这些文件进行管理。一个进程可以打开多个文件,那么操作系统如何让进程和其打开的文件映射起来呢?
在PCB中存放一个管理该进程打开的文件的结构体,其中有一部分是数组,专门存放struct_file指针,指向在内存中的文件结构体。而操作系统通过对文件描述,组织,进行管理,就是对sruct_file结构体进行管理。其中有文件属性等内容。其中一个文件可能会被多个进程使用,所以stuct_file结构体中会有一个计数器,有一个进程使用它就+1。当一个进程要关闭一个文件时,将自己对应的struct files_struct中的文件数组位置置为NULL,对应文件struct_file中的计数器减1,当为0时,可以将对应的struct_file从队列中删除。

不要有疑问,0,1,2->stdin,stdout,stderr,键盘,显示屏,虽然都是硬件,但是在linux中也可以作为文件管理。是怎样作为文件使用的呢?


可以看到在linux源码中struct_file存在operations指针,系统可以通过基类struct_file调用不同设备的read和write函数,是一种“多态”!


文件描述符的分配规则
遍历files_struct的数组,找到一个最小的且未被使用的下标,分配给新的文件。
重定向
fprintf和fscanf
| 函数名 | 功能方向 | 默认文件流 | 底层系统调用 | 示例 | 输出/输入去向 |
|---|---|---|---|---|---|
printf | 输出(写) | stdout(标准输出) | write(1, ...) | printf("Hello\n"); | 输出到终端 |
fprintf | 输出(写) | 任意 FILE *(文件、stderr 等) | write(fd, ...) | fprintf(fp, "data=%d\n", x); | 输出到文件或指定流 |
scanf | 输入(读) | stdin(标准输入) | read(0, ...) | scanf("%d", &x); | 从键盘读取 |
fscanf | 输入(读) | 任意 FILE * | read(fd, ...) | fscanf(fp, "%d", &x); | 从文件读取 |




引入
先看一个有趣的代码:
#include<stdio.h>
#include<sys/types.h>
#include<fcntl.h>
#include<sys/stat.h>
#include<unistd.h>
int main()
{
close(1);
int fd = open("log.txt",O_RDWR|O_CREAT,0666);
if(fd < 0)
{
perror("open");
return 0;
}
fprintf(stdout,"这是一个进程");
fflush(stdout);
close(fd);
}
最后它并不会在显示器上打印“这是一个进程”,而是将其写入到log.txt文件中。 那么为什么会出现这种现象?那是因为发生了重定向,本来应该是向某个文件输入,但是输入到另一个文件。

dup函数
上图中的所有数据都是内核数据结构,只有(系统调用)操作系统才有权限提供接口修改内核数据结构。最常用的就是dup函数。
比较常用的就是dup2函数,oldfd为原来文件的描述符,newfd为重定向输出的文件描述符。
#include<stdio.h>
#include<sys/types.h>
#include<fcntl.h>
#include<sys/stat.h>
#include<unistd.h>
int main()
{
int fd = open("log.txt",O_RDWR|O_CREAT,0666);
if(fd < 0)
{
perror("open");
return 0;
}
//和上面的代码是等效的
dup2(fd,1);
fprintf(stdout,"这是一个进程");
fflush(stdout);
close(fd);
}
>(1>)、>>(1>>)、2>、2>>、2>&1
| 符号 | 含义 | 默认操作的文件描述符 |
|---|---|---|
> | 输出重定向(覆盖) | 1> |
>> | 输出重定向(追加) | 1>> |
< | 输入重定向 | 0< |
2> | 错误输出重定向(覆盖) | 2> |
2>> | 错误输出重定向(追加) | 2>> |
2>&1 | 将错误输出重定向到标准输出相同的位置 | — |


perror输出成这是perror: Success,是因为perror不像printf那样单纯打印字符串,它的作用是:输出参数字符串 + 当前 errno 对应的错误信息。在这段程序中,前面没有出错操作,所以全局变量errno = 0,对应的错误信息是"Success"。因此输出为:这是perror: Success如果有系统调用出错(如
fopen失败),errno会被设置为错误码,比如2,则输出会变成:这是perror: No such file or directory
> 与 1>
作用:
将标准输出(fd=1)重定向到文件(覆盖写)。

-
stdout(正常输出)写入到out.txt;stderr(错误信息)仍显示在屏幕
>> 与 1>>
作用:
将标准输出追加到文件末尾。

效果:
-
输出追加到
out.txt;错误仍在屏幕显示
2> 与 2>>
作用:
把标准错误输出(fd=2)重定向到文件。

效果:
-
错误信息进入
err.txt;正常输出仍显示在屏幕
2>&1
作用:
把 stderr(fd=2)重定向到 stdout(fd=1) 当前指向的地方。

执行顺序(从左到右解析):
> out.txt:让标准输出(1)指向out.txt
2>&1:让标准错误(2)也指向与 1 相同的文件
正常输出 + 错误输出 全部进 out.txt,终端上不再显示任何输出
等价写法:
总结表格
| 命令 | 标准输出(stdout) 去向 | 错误输出(stderr) 去向 | 文件写入方式 |
|---|---|---|---|
./a.out > out.txt | out.txt | 屏幕 | 覆盖 |
./a.out >> out.txt | out.txt | 屏幕 | 追加 |
./a.out 2> err.txt | 屏幕 | err.txt | 覆盖 |
./a.out 2>> err.txt | 屏幕 | err.txt | 追加 |
./a.out > all.txt 2>&1 | all.txt | all.txt | 覆盖 |
./a.out >> all.txt 2>&1 | all.txt | all.txt | 追加 |
<
在 Linux 中,重定向符号 < 表示 把文件的内容作为标准输入(stdin)提供给程序



把in.txt绑定到文件描述符 0(stdin)上,让程序从文件中读输入,而不是从键盘读。
上面所写的全部重定向命令中,
2>&1、>out.txt、<in.txt都是 Shell 重定向子句,它们是 Shell 的语法结构,用来告诉 Shell:“在执行命令前,修改某个文件描述符(fd)的来源或去向。”
比如./test < input.txt 2>&1 > output.txt,命令是./test,< input.txt 是它的输入重定向,2>&1把
stderr(fd=2)重定向到stdout(fd=1) 当前指向的地方,> output.txt是他的输出重定向。
底层原理:dup2
这些符号本质上都是在 执行 dup2 系统调用。
./test > out.txt 2>&1 < in.txt
并不是
test程序在做dup2(),而是 Shell 调用了dup2()来修改文件描述符表。也就是说:
你在键盘上敲的重定向符号 → 由 Shell 解释;
Shell 用
open()打开或创建文件;Shell 用
dup2()改变文件描述符的指向;然后才执行
execve("./test", ...)启动程序;启动后的程序就“天然地”从这些文件中读写。
示例 1:./test > out.txt
Shell 做的事大致等价于:
int fd = open("out.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
dup2(fd, 1); // 把stdout重定向到文件
close(fd);
execve("./test", argv, envp);
所以程序内部的:
printf("Hello\n");
其实是系统调用:但是此时1已经被fd夺舍了,所以再往1中写就会往out.txt写入。
write(1, "Hello\n", 6);
理解缓冲区
在操作系统有一份缓冲区,在语言级别也会有一份缓冲区。
系统缓冲区
下面的图是在系统级别上的缓冲区,每一个sruct_file会对应一个缓冲区。

语言缓冲区
C语言中会将文件描述符还有其它一些文件信息封装成FILE,在这个FILE中,其实还存在语言缓冲区:
struct _IO_FILE {
unsigned char *_IO_buf_base; // 缓冲区起始地址
unsigned char *_IO_buf_end; // 缓冲区结束地址
unsigned char *_IO_write_ptr; // 当前写指针
int _flags; // 缓冲模式 (行缓冲、全缓冲、无缓冲)
int _fileno; // 文件描述符(如stdout=1)
...
};
所以当在调用fopen时,会返回一个FILE*类型的指针,因为fopen中会malloc一个FILE类型的结构体。因此,语言层面上的缓冲区是存在内存中的堆上。
当在语言层面上往文件写内容时,会先往缓冲区中写,然后等待刷新后会被刷新到系统上的缓冲区中。这里有两点,语言层面上的缓冲区的刷新策略是什么?被刷新到系统的缓冲区上时就一定会被刷新到硬件中吗?
模式 说明 典型场景 行缓冲 输出遇到 \n自动刷新stdout(连接终端时) 全缓冲 缓冲区满才刷新 文件 I/O 无缓冲 每次写立即刷新 stderr(错误输出)
其次是当进程结束时,缓冲区的内容会被刷新到系统中。
其实并不会立即刷新到硬件上,除非显式刷新,调用fsync(fd);或者当关闭fd时,会刷新。
看几个例子加深对缓冲区的理解:
1.

动态图理解过程:
第一个例子的实际运行时内存的动态图,正常执行带\n的printf和fprintf,write:


当不带\n时:



最后进程退出时:
2.
printf内,先解析格式字符串,取变长参数(如有必要),格式化成字符串,再向FILE中的缓冲区写入,如果缓冲区满 / 遇到 '\n' / fflush() / 程序结束,会调用write,向系统写入。fprintf也是如此。
标准错误和标准输出的区别
标准错误和标准输出都是向显示器打印,有什么区别呢?标准错误和标准输出是通过不同的文件描述符打印到显示器上的!
在发生重定向时,没有说明默认就是重定向标准输出,如果要重定向标准错误则要加上2:


意义在哪里?可以区分程序中的日常信息和错误信息!方便查找错误。cout和cerr,printf和perror要分开使用。
如果不想分开这两个内容,想重定向到一个文件中:

先将1重定向到all.txt中,再把1的地址给2,那么2也指向1所指的文件,2也完成了重定向。

hh

782

被折叠的 条评论
为什么被折叠?



