输入设备:
输出设备:
文件IO:
文件I/O(Input/Output)是指计算机程序与文件系统进行数据交互的过程。这包括从文件中读取数据(输入)和将数据写入文件(输出)。在编程语言中(以C语言为例),语言会提供相应的库函数,在Liunx中,会提供一些系统调用接口。
在C语言中,文件I/O 主要涉及使用标准I/O库(stdio.h)提供的函数。以下是一些常见的C语言文件I/O 操作:
#include
int main()
{
FILE *file = fopen("example.txt", "r");
if (file == NULL) {
// 处理文件打开失败的情况
}
return 0;
}
int fclose(FILE *stream);
int main()
{
FILE *file = fopen("example.txt", "r");
if (file == NULL) {
// 处理文件打开失败的情况
}
//操作文件
fclose(file);//关闭文件
return 0;
}
fwrite
用于二进制文件,fprintf
用于文本文件。int main()
{
FILE *file = fopen("example.txt", "r");
if (file == NULL) {
// 处理文件打开失败的情况
}
// 向打开的文件写入文本
fprintf(file, "Hello, World!");
fclose(file);//关闭文件
return 0;
}
fread
用于二进制文件,fgets
用于文本文件。int main()
{
FILE *file = fopen("example.txt", "r");
if (file == NULL) {
// 处理文件打开失败的情况
}
char buffer[100];
// 向打开的文件读取一行文本存到buffer数组中
fgets(buffer, sizeof(buffer), file);
fclose(file);//关闭文件
return 0;
}
示例:
#include
#include
int main()
{
FILE *fp = fopen("myfile", "w");//w的方式打开,如果文件不存在,则创建文件
if(!fp)
{
printf("fopen error!\n");
}
const char *msg = "hello World!\n";
int count = 5;
while(count--)
{
fputs(msg,fp);//向打开的文件写入五次hello World!
}
fclose(fp);
return 0;
}
运行结果:
#include
#include
int main()
{
FILE *fp = fopen("myfile", "r");
if(!fp)
{
printf("fopen error!\n");
}
char buffer[64];
for (int i = 0; i < 5; i++){
//将读取的文件存放到数组中
fgets(buffer, sizeof(buffer), fp);
printf("%s",buffer);
}
fclose(fp);
return 0;
}
在C语言中,有三个标准I/O流,它们是由标准库提供的默认打开的流。
stdin
(标准输入):
stdin
是标准输入流,通常关联于键盘。程序可以通过 scanf
或 fgets
等函数从 stdin
读取用户的输入。stdout
(标准输出):
stdout
是标准输出流,通常关联于显示屏或终端。程序可以通过 printf
等函数向 stdout
输出信息。stderr
(标准错误):
stderr
是标准错误输出流,也通常关联于显示屏或终端。与 stdout
类似,但主要用于输出错误信息。在程序出现错误时,可以使用 fprintf(stderr, ...)
将错误信息输出到 stderr
。这三个流在程序启动时由操作系统自动打开,不需要用户手动打开或关闭。它们是标准库中预定义的 FILE
结构的指针。在头文件 stdio.h
中定义了它们。
c程序运行后,默认打开这三个流,我们才能使用printf,scanf 向显示器打印,从键盘读取输入等操作。
Linux下一切皆文件,显示器也可以当做文件,fputs可以向磁盘文件进行写入,那么也可以向显示器文件进行写入。
示例
int main()
{
fputs("Hello World!\n",stdout);
return 0;
}
操作文件,除了C语言的接口可以,其他语言(C++/Java)都有自己的一套接口操作文件,语言级别的接口都是封装了系统调用接口来完成的。
Linux下常见对文件操作的系统调用接口
int open(const char *pathname, int flags);
int close(int fd);
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
示例:以只读的方式新建文件
#include
#include
#include
#include
#include
int main()
{
int fd = open("log.txt",O_RDONLY | O_CREAT);//加O_CREAT才会新建文件,否则不会新建
return 0;
}
新建的log.txt文件访问权限出错
设置第三个参数,只读新建文件的访问权限
int main()
{
//创建出来文件的权限值不受umask的影响
umask(0);
int fd = open("log.txt",O_RDONLY | O_CREAT,0666);//0666对应-rw-rw-rw
return 0;
}
运行结果
open打开成功返回文件描述符,close通过文件描述符关闭文件
int main()
{
//创建出来文件的权限值不受umask的影响
umask(0);
int fd = open("log.txt",O_RDONLY | O_CREAT,0666);//0666对应-rw-rw-rw
close(fd);//关闭文件
return 0;
}
从文件中读取信息
ssize_t read(int fd, void *buf, size_t count);
int main()
{
int fd = open("log.txt",O_RDONLY);
char buf[1024];
ssize_t nums =read(fd,buf,sizeof(buf));//从文件中读取信息存放到buf数组中
printf("%d\n",nums);
printf("%s",buf);
return 0;
}
向打开的文件中写入数据
ssize_t write(int fd, const void *buf, size_t count);
fd
是文件描述符,表示已打开文件或其他I/O对象。buf
是指向要写入的数据的指针。count
是要写入的字节数int main()
{
int fd = open("log.txt", O_WRONLY | O_APPEND);
if(fd < 0 ){
perror("open");
}
const char* str = "Hello Wolrd!\n";
//向log.txt文件写入Hello Wolrd!
ssize_t nums = write(fd,str,strlen(str));
printf("%d",nums);//实际写入字节数
close(fd);
return 0;
}
运行结果:
进程要访问文件,必须要先打开文件,文件要被访问,前提是加载到内存中,才能被访问。因为冯诺依曼体系结构规定CPU不能直接访问外设(磁盘),必须先将数据加载到内存中,让CPU访问内存。所以文件要被访问,前提是加载到内存中。
一个进程可以打开多个文件,当系统中存在大量的被打开的文件,操作系统要将这些打开的文件管理起来(用struct描述起来,再用特定的数据结构管理起来)
操作系统会为每个已经打开的文件创建各自的struct file结构体,然后将这些结构体以双链表的形式连接起来,之后操作系统对文件的管理也就变成了对这张双链表的增删查改等操作。
当程序运行起来,操作系统会该进程创建对应的PCB、页表、进程地址空间(mm_struct)等
文件要被进程访问,就要建立进程和文件之间的关系。用来区分哪些进程访问哪些打开的文件。
当文件存储在磁盘当中时,我们将其称之为磁盘文件,而当磁盘文件被加载到内存当中后,我们将加载到内存当中的文件称之为内存文件。磁盘文件和内存文件之间的关系就像程序和进程的关系一样,当程序运行起来后便成了进程,而当磁盘文件加载到内存后便成了内存文件。
磁盘文件由两部分构成,分别是文件内容和文件属性。文件内容就是文件当中存储的数据,文件属性就是文件的一些基本信息,例如文件名、文件大小以及文件创建时间等信息都是文件属性,文件属性又被称为元信息。
文件加载到内存时,一般先加载文件的属性信息,当需要对文件内容进行读取、输入或输出等操作时,再延后式的加载文件数据。
写代码观察一些open的返回值,
int main()
{
//创建出来文件的权限值不受umask的影响
umask(0);
int fd1 = open("log1.txt",O_RDONLY | O_CREAT,0666);//0666对应-rw-rw-rw
int fd2 = open("log2.txt",O_RDONLY | O_CREAT,0666);
int fd3 = open("log3.txt",O_RDONLY | O_CREAT,0666);
int fd4 = open("log.4txt",O_RDONLY | O_CREAT,0666);
//打开一个不存在的文件
int fd5 = open("xxx",O_RDONLY);
printf("fd1 = %d\n",fd1);
printf("fd2 = %d\n",fd2);
printf("fd3 = %d\n",fd3);
printf("fd4 = %d\n",fd4);
printf("fd5 = %d\n",fd5);
return 0;
}
运行结果:
c程序运行时 默认打开三个流,当进程创建时就默认打开了标准输入流、标准输出流和标准错误流,也就是说数组当中下标为0、1、2的位置已经被占用了,所以只能从3开始进行分配。
当关闭0号fd后再打开文件,新的fd是怎么样的呢?
int main()
{
umask(0);
close(0);//关闭stdin
int fd1 = open("log1.txt",O_CREAT | O_RDONLY,0666);
int fd2 = open("log2.txt",O_CREAT | O_RDONLY,0666);
int fd3 = open("log3.txt",O_CREAT | O_RDONLY,0666);
printf("fd1 = %d\n",fd1);
printf("fd2 = %d\n",fd2);
printf("fd3 = %d\n",fd3);
return 0;
}
运行结果:
关闭0号文件时,新建的文件log1.txt会从0开始,后面新建的继续从3开始
文件描述符的分配规则: 在数组当中,找到当前没有被使用的 最小的一个下标,作为新的文件描述符。
了解了文件描述符和文件描述符的分配规则之后,在理解重定向就很简单了。
重定向的本质就是在操作系统内部,更改fd对应内容的指向。
常见的重定向有:
>
输出重定向>>
追加输出重定向<
输入重定向小例子:
关闭1号文件,再向显示器打印信息
int main()
{
close(1);
int fd = open("log.txt",O_WRONLY | O_CREAT,0666);
if(fd < 0 ){
perror("open fail");
return -1;
}
printf("Hello World!\n");
printf("Hello World!\n");
close(fd);
return 0;
}
运行结果:
运行程序,什么也没打印,这是因为printf会向1号文件输出信息,但是1号文件默认是显示器,现在已经关闭了1号文件。1号文件变为了我们自己的文件。这种本应该向显示器输出信息,却输出到了别的文件,这就叫做输出重定向(fd = 1)。
理论上log.txt文件会存放Hello World! Hello World! 的信息,但是当我们打开log.txt文件的时候,里面仍然是空的
这是因为缓冲区的问题,printf并不会直接把数据写入到内存中,而是先写入到缓存区中,当printf写入完后,使用fflush刷新一下缓冲区就可以了。
int main()
{
close(1);
int fd = open("log.txt",O_WRONLY | O_CREAT,0666);
if(fd < 0 ){
perror("open fail");
return -1;
}
printf("Hello World!\n");
printf("Hello World!\n");
fflush(stdout);
close(fd);//刷新缓冲区
return 0;
}
重新编译,运行程序,查看文件,就能看见信息了
输入重定向就是从一个文件读取数据,现在重定向为另一个文件。
其本质就是改变fd(数组下标)指向的内容
例子:
将从键盘读取的数据,重定向到指定文件中读取
int main()
{
close(0);
int fd = open("log.txt",O_RDONLY);
char str[80];
while(scanf("%s",str) != EOF)
{
printf("%s\n",str);
}
close(fd);
return 0;
}
追加输出重定向的区别:
输出重定向会覆盖原有的内容重写。而追加输出据基础上继续新增
//追加重定向
int main()
{
close(1);
int fd = open("log.txt",O_WRONLY | O_APPEND | O_CREAT , 0666);
if(fd < 0){
perror ("open fail");
return -1;
}
printf("Hello Linux!\n");
printf("Hello Linux!\n");
printf("Hello Linux!\n");
fflush(stdout);
close(fd);
return 0;
}
运行结果:
删除open的O_APPEND
选项后 运行结果:
stdout和stderr 区别:
stdout 和 stderr 对应的默认都是显示器文件,但是区别就在于:重定向的是文件描述符是1的标准输出流,而并不会对文件描述符是2的标准错误流进行重定向。
dup2函数是Linux提供的系统调用接口
函数原型:
它的作用是将 oldfd
复制到 newfd
,并且如果 newfd
已经打开,则会先关闭它。这可以用于重定向文件描述符。
int main()
{
int fd = open("log.txt",O_WRONLY);
dup2(fd,1);//将fd更改为1
printf("Hello Linux!!!!!!!!!!\n");
printf("Hello Linux!!!!!!!!!!\n");
printf("Hello Linux!!!!!!!!!!\n");
fflush(stdout);
close(1);
return 0;
}
FILE
是一个在C语言中定义的结构体,用于处理文件操作。FILE
结构是由标准C库提供的,用于在程序中表示文件流。它包含了有关文件的信息,如文件描述符、缓冲区等。
不论是语言级别的库函数还是系统调用级别的函数,访问文件的本质就是通过文件fd进行访问的
C库中的FILE结构体也一定封装了fd
可以在Linux中 /usr/include/stdio.h
路径下查看stdio.h中的源码
typedef struct _IO_FILE FILE;//源码中的描述
FILE实际上就是struct _IO_FILE的一个别名
了解了FILE之后,当我们使用fopen打开一个文件之后,
fopen首先创建FILE结构体变量,并且返回该结构体的地址(FILE*),然后通过系统调用 open接口打开对应的文件,得到对应的文件描述符。将对应的文件描述符填入到FILE结构体中。
然后fprintf,fread,fwrite等函数通过传入对应的FILE* 指针找到对应的FILE结构体,从FILE中拿到对应的文件描述符,最后通过文件描述符对文件进行操作。
观察下面代码
#include
#include
int main()
{
printf("Hello printf\n");
fputs("Hello fputs\n",stdout);
write(1,"Hello write\n",12);//这里不能用stdout stdout是c语言的 系统不认识
fork();//创建子进程
return 0;
}
分别用语言级别的函数和系统调用级别的函数向显示器打印信息
运行结果:成功向显示器打印信息
但是 如果将运行结果重定向到一个文件中,结果变为如下情况:
这是为什么呢?
首先了解一下缓冲区的刷新策略
一般分为以下几点
还有两种特殊情况:
而显示器文件的刷新策略是行刷新。磁盘文件的刷新策略是满刷新(这样为了更少的IO操作,更少的外设访问,从而提高效率所有的外设更倾向于满刷新)
当执行上面的程序时,向显示器打印就是采用行刷新的策略,每个打印信息后面都有\n进行刷新缓冲区。当执行完代码就立即打印到显示器上了。
然而当输出重定向到log.txt(磁盘文件中时,缓冲区的刷新策略变为了满刷新)不会立即输出到文件中。使用printf函数和fouts函数打印的数据都存到了语言自带的缓冲区了。之后在用fork创建子进程,当子进程执行结束时,进程退出,要刷新缓冲区的内容。向文件进行写入。由于进程之间具有独立性,此时就需要对数据进行写时拷贝,缓冲区的数据就会变为父子进各自一份。父进程执行完毕,进程退出,刷新缓冲区,又向文件进行写入。所以printf和fputs输出两遍。而wiret只执行了一次,证明没有所谓的缓冲区。
缓冲区由谁提供
实验证明这个缓冲区是C标准库提供的,如果说这个缓冲区是操作系统提供的,那么printf、fputs和write函数打印的数据重定向到文件后都应该打印两次。
缓冲区在哪
缓存区就是一段内存空间,FILE 结构体中关于缓冲区的描述。和进程地址空间的描述类似
//缓冲区相关
/* The following pointers correspond to the C++ streambuf protocol. */
/* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
char* _IO_read_ptr; /* Current read pointer */
char* _IO_read_end; /* End of get area. */
char* _IO_read_base; /* Start of putback+get area. */
char* _IO_write_base; /* Start of put area. */
char* _IO_write_ptr; /* Current put pointer. */
char* _IO_write_end; /* End of put area. */
char* _IO_buf_base; /* Start of reserve area. */
char* _IO_buf_end; /* End of reserve area. */
/* The following fields are used to support backing up and undo. */
char *_IO_save_base; /* Pointer to start of non-current get area. */
char *_IO_backup_base; /* Pointer to first valid character of backup area */
char *_IO_save_end; /* Pointer to end of non-current get area. */
为社么要有缓冲区
就是为了更少的IO操作,更少的外设访问,从而提高效率
操作系统也有缓冲区
当我们刷新用户缓冲区的数据时,并不是直接将用户缓冲区的数据刷新到磁盘或是显示器上,而是先将数据刷新到操作系统缓冲区,然后再由操作系统将数据刷新到磁盘或是显示器上。因为操作系统才是软硬件资源的管理者。
文件不仅仅有内容,还有其属性(文件大小,类型,拥有者等)。文件被存放到磁盘当中。现在基本都是固态硬盘了。这里以磁盘为例说明。
磁盘是一种存储介质,在计算机中,磁盘是唯一的机械设备。
磁盘分区:
磁盘通常被称为块设备,一般以扇区为单位,一个扇区的大小通常为512字节。比如一个512GB的磁盘,扇区大概有10亿多个。
这和数组很像,为了方便理解,可简单的理解为:
将数据存储到磁盘中转换为将数据存储到数组中。
找到磁盘特定的扇区变为找到特定数组的下标。
对磁盘的管理变为了对数组的管理。
操作系统为了更好的管理磁盘,对磁盘进行了分区操作。(这个原理就和一个国家划分省一样)。
在windows系统当中,经常会对磁盘进行分区,分为C盘,D盘等。就是在原有的基础上进一步划分区域(和一个省份继续划分市级单位一样)。
对磁盘的管理变成了对小分区的管理。
在linux中 也可以查看分区信息。
使用ls /dev/vda* -l
命令查看
区域划分完成了,下一步就要对其进行管理了。对磁盘的第一个操作就是我们平时所熟悉的格式化。
磁盘格式化:就是对磁盘中的分区进行初始化的一种操作。磁盘格式化就是对分区后的区域写入对应的管理信息。(就相当于为每个市分配一个市长)
写入的管理信息具体是什么,这个由不同的操作系统决定,常见的文件系统包括FAT32、NTFS、exFAT(在Windows系统中常见)、EXT2(在Linux系统中常见)、HFS+(在Mac OS中常见)。
EXT2(第二扩展文件系统)是Linux操作系统中使用的一种文件系统。它是EXT文件系统家族的一部分,后来被EXT3和EXT4所取代。基本原理都一样。EXT4加入了更多的功能。这里以EXT2为例。
操作系统为了更好的管理磁盘,对磁盘进行了分区。分区完对其格式化,写入文件系统。
文件系统通常是每个分区都有的。分区的头部会包括一个启动块(Boot Block),对于该分区的其余区域,EXT2文件系统会根据分区的大小将其划分为一个个的块组(Block Group)。
注意:D盘也有同样的文件系统。
启动块的大小是确定的,而块组的大小是由格式化的时候确定的,并且不可以更改。
EXT2文件系统会根据分区的大小划分为数个Block Group。而每个Block Group都有着相同的结构组成。每个组块都由超级块(Super Block)、块组描述符表(Group Descriptor Table)、块位图(Block Bitmap)、inode位图(inode Bitmap)、inode表(inode Table)以及数据表(Data Block)组成。如下图
通过上面对文件系统的了解,可以看出文件的属性和内容是分开存放的。当系统中存在大量的文件的时候,需要给每个文件的属性集起一个唯一的编号,即inode号。也就是说,inode是一个文件的属性集合,Linux中几乎每个文件都有一个inode。
inode
(Index Node的缩写)是操作系统中用于存储文件属性的数据结构。每个文件或目录都有一个唯一的inode,通过inode可以查找和管理文件的相关信息,而不是直接使用文件名。inode
可以通过文件系统的 inode表
中确定。
使用ll -i
可以查看文件的inode
当我们新建一个文件并对其写入内容时 主要分为以下四步
删除文件
删除文件并不会像我们想象的那样,将磁盘中的数据抹除,而是将