一、系统文件IO
1.文件与读写字符串
对于C文件接口,假如想向特定文件写入字符串:
cFile.c
#include<stdio.h>
int main()
{
FILE * fp = fopen("./log.txt","w");
if(NULL == fp)
{
perror("fopen error");
return 1;
}
int count = 5;
while(count--)
{
const char *msg = "cFile\n";
fputs(msg,fp);
}
fclose(fp);
return 0;
}
执行结果如下,查看log.txt文件:

假如想从特定文件读字符串,从log.txt中按行读取,读取的内容放在缓冲区,如果fgets读取成功,返回读取的新字符串的地址,如果读取失败就返回NULL,feof用来判断文件是否正常退出,即fgets是否读取成功:
cFile.c
#include<stdio.h>
int main()
{
FILE *fp = fopen("./log.txt","r");
if(NULL == fp)
{
perror("fopen error");
return 1;
}
char buffer[64];
while(fgets(buffer,sizeof(buffer),fp))
{
printf("%s",buffer);
}
if(!feof(fp))
{
printf("fgets quit not normally!\n");
}
else
{
printf("fgets quit normally!\n");
}
fclose(fp);
return 0;
}
打印buffer字符串:

fprintf用于格式化地向一个文件中写入内容,fscanf用于格式化地从一个文件中读取。
2.标准输入、标准输出、标准错误
C程序会默认打开三个输入输出流:stdin, stdout, stderr,C语言会把标准输入输出流和标准错误当文件处理,流就是向硬件记录:

例如,fputs的第二个参数就是流,可以向标准屏幕stdout写入:
standardFile.c
#include<stdio.h>
int main()
{
FILE *fp =fopen("./log.txt","w");
const char *message = "this is a log file!\n";
fputs(message,stdout);
fclose(fp);
return 0;
}
执行结果如下:

这种将本该显示到显示器的内容显示到文件里的操作也叫做输出重定向。既然能打印内容到标准输出,那么是否可以打印到stderr呢?请看下面代码:
standardFile.c
#include<stdio.h>
int main()
{
FILE *fp =fopen("./log.txt","w");
const char *message = "this is a log file!\n";
fputs(message,stderr);
fclose(fp);
return 0;
}
执行结果如下,可以看到结果也打印在了显示器上,但是把stderr的内容重定向到文件时,却没有重定向成功,虽然都往显示器上打印,但是out和err重定向时是不一样的。:

由于linux下一切皆文件,fputs向一般文件或者硬件设备都能写入,所以fputs向磁盘也可以写入。
C++中的标准输入、标准输出、标准错误分别是cin、cout、cerr。基本上大部分语言都会提供这三个输入输出,因为写代码时,需要向程序输入数据,得有结果和错误,默认就把标准输入、标准输出和标准错误打开了。
刚刚c语言的所有操作都是在向硬件写入,而硬件包括显示器、磁盘、键盘、鼠标,由于操作系统时硬件和驱动的管理者,所以所有语言对文件的操作都必须贯穿操作系统。

而由于操作系统不相信任何人,要访问操作系统,就需要通过系统调用接口(system call),几乎所有语言都对操作系统的读写操作进行了封装,才有了语言级别的fopen、fclose、fread、fwrite、fgets、fputs、fgetc、fputc函数,这些函数底层都使用了OS提供的系统调用。
3.系统调用文件操作函数
(1)open
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.f>
int open(const char *pathname,int flags,mode_t mode);
第一个参数pathname是文件名
第二个参数flags是打开方式,是32位的bit位,每一个bit位,代表一个标志,可以通过位操作的方式,一次向系统传递多个标志位,位操作在系统中很高效。查询fcntl-linux.h可以看到每个标志位的含义

第三个参数mode设置新建文件的权限。
返回值是int类型,如果执行成功,则返回新的文件描述符,如果执行失败,则返回-1。
c语言的fopen函数是对open 函数做了封装,所以在C文件中写文件操作直接就用C的接口。
(2)close
#include<unistd.h>
int close(int fd);
其中,参数fd是文件描述符,要关闭文件时,直接传入文件描述符即可。
(3)read
从打开的文件中读取内容:
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
第一个参数fd是打开的文件描述符
第二个参数buf是把读取到的内容存入的变量
第三个参数count是从从打开的文件中读取多少个字节
返回值类型是ssize_t,是有符号整型,在32位机器上等同与int,size_t 就是无符号型的ssize_t。返回-1表示读取失败;返回正整数代表成功读取的字节数。
如下,想把log.txt中的文件内容全部读取到变量buffer中:

readProcess.c
#include<stdio.h>
#include<unistd.h>
#include<fcntl.h>
int main()
{
int fd = open("./log.txt",O_RDONLY ,0644);
if(fd < 0)
{
perror("open error!\n");
return 1;
}
char buffer[1024];
ssize_t s = read(fd,buffer,sizeof(buffer)-1);//文件不需要用\0来标记字符串结束
if(s > 0)
{
buffer[s] = 0;//读取成功后,手动字符串结尾加上\0
printf("%s\n",buffer);
}
close(fd);
return 0;
}
读取结果如下:

(4)write
向打开的文件中写入内容:
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
第一个参数fd是打开的文件描述符
第二个参数buf是把的缓冲区buf中的内容写入到文件
第三个参数count是向文件中写入buf的count个字节
返回-1表示写入失败;返回正整数,表示成功向文件写入的字节数。
注意:
当向文件中写入字符串时,不需要写入字符串末尾的\0,因为文件关心的是字符串的内容,\0仅仅只是字符串结束的标志位,并不是需要写入到文件的内容,所以不需要把\0写进去。
如下代码,将字符串写入到文件中:
#include<stdio.h>
#include<fcntl.h>
#include<unistd.h>
#include<string.h>
int main()
{
int fd = open("./log.txt",O_WRONLY|O_CREAT,0644);
if(fd < 0)
{
perror("open error!\n");
}
char *buffer = "OK,just as the sushine\n";
ssize_t s = write(fd,buffer,strlen(buffer));//strlen没有包含\0,不需要把\0写进去
return 0;
}
执行结果如下:

二、文件描述符
1.什么是文件描述符
open函数的返回值会返回一个文件描述符,可以看看这个文件描述符的值是多少:
#include<stdio.h>
#include<unistd.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<fcntl.h>
int main()
{
int fd = open("./log.txt",O_CREAT | O_RDONLY,0644);
if(fd < 0)
{
printf("open error!\n");
}
printf("fd = %d\n",fd);
close(fd);
return 0;
}
发现fd=3:

那如果多打开几个文件呢?文件描述符的值各是多少?
#include<stdio.h>
#include<unistd.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<fcntl.h>
int main()
{
int fd1 = open("./log1.txt",O_CREAT | O_RDONLY,0644);
int fd2 = open("./log2.txt",O_CREAT | O_RDONLY,0644);
int fd3 = open("./log3.txt",O_CREAT | O_RDONLY,0644);
int fd4 = open("./log4.txt",O_CREAT | O_RDONLY,0644);
int fd5 = open("./log5.txt",O_CREAT | O_RDONLY,0644);
if(fd1 < 0 || fd2 < 0 || fd3 < 0 || fd4 < 0 || fd5 < 0)
{
printf("open error!\n");
}
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);
close(fd1);
close(fd2);
close(fd3);
close(fd4);
close(fd5);
return 0;
}
执行结果如下,发现文件描述符从3开始递增了:

文件描述符从3开始,那0、1、2去哪里了呢?
0标准输入:键盘
1标准输出:显示器
2标准输出:显示器
Linux进程默认情况下会有3个缺省打开的文件描述符,分别是标准输入0, 标准输出1, 标准错误2
因此连续打开多个文件时,底层给文件分配的文件描述符分别是:

这说明文件描述符就是一个数组的下标。
所有的文件操作,表面上看起来是进程执行对应的函数,也是进程对文件的操作,操作文件就必须先打开文件,并且将文件的相关属性信息加载到内存,而操作系统中存在大量的进程,且一个进程可以打开多个文件,有可能进程数:打开的文件数=1:n,那么操作系统就需要把打开的文件在内存也就是系统中管理起来,管理的方式------先描述,再组织。
打开的文件在内存中,由操作系统做管理。文件没有打开时在磁盘上,由文件系统做管理,这时即便是一个空文件也需要占用磁盘空间的。比如当我们创建一个空文件时:

虽然它的大小是0KB,没有内容,但是它的属性是存在的,比如文件名称、修改日期、类型、大小、安全性能、详细信息、所有者、所属组、权限:

从以上可以看出,当我们创建一个文件时,文件要占用磁盘,文件有属性,属性也是数据。磁盘文件既包含文件内容,又包含文件属性。
之前对文件的所有操作,既有对文件内容的操作,也有对文件属性的操作。其中对文件内容的操作包括fread、fwrite、fgets、fputs。对文件属性的操作包括ftail、rewind、fseek、chmod、chgrp、mv更改文件名称。
操作系统管理文件采用先描述再组织的方式,那么操作系统是如何描述的?又是如何组织的呢?
打开文件时,要在内核里面设计对应的结构体,先把打开的文件描述起来,在内核当中描述打开的文件的结构体叫做struct file,文件在磁盘上已经有属性了,打开文件不过是把文件属性加载到特定内存,要加载一些内存当中特有的属性。因此当文件被打开时,在操作系统内部一定会维护struct file来表示一个已经被打开的文件——先描述。
struct file
{
//文件内容
//文件属性
}
打开文件时,一定是先创建进程,这个进程可以打开多个文件,内核当中操作系统就要帮我们为每一个已经打开的文件,建立一个struct file结构,这个结构包含了文件的相关属性信息,如果一个进程打开了多个文件,那么在系统中会存在大量的struct file结构,操作系统会以双向链表的形式把所有struct file全部链接起来。

当操作系统管理进程时,有进程的列表。先描述再组织相关结构,如果要管理文件,也有文件相关的列表。打开的这么多文件,如何知道哪一个进程对应哪些文件?操作系统为了能够让进程和文件产生关联,让进程在内核当中包含一个结构file_struct,这个结构里包含了指针数组,所以task_struct也就是pcb当中包含一个指针指向file_struct的地址,数组里面包含的内容全部都是struct file类型的指针。

当打开文件时,把文件描述符描述起来,或者把文件和相关的struct对象描述起来之后,进程和文件如何关联?数组有0123下标,把对应的描述文件的结构体变量的地址写入到特定的下标里,这就相当于fd_array[0]指向了第一个文件。0、1、2文件描述符分别被申请成了保存键盘文件,显示器文件、显示器文件,保存着这三个文件各自的地址,对应标准输入、标准输出、标准错误。上层在调用这三个文件的时候就直接使用0、1、2即可调用。
因此当用户打开这三个文件之外的文件时,文件在磁盘中就被打开了,在内存里就需要形成struct_file结构,再把struct_file的文件描述符3分配给这个文件,会把这个结构体的地址填入下标为3的文件描述符内:

再把3返回给上层用户,此时就拿到了3这个下标。
成功打开文件的目的是要进行写入或者读取,不管是写入还是读取都是系统调用,且第1个参数都是文件描述符fd,那么问题来了。
在执行read或write的时候为什么要执行文件描述符fd呢?
进程执行read或write系统调用的同时把文件描述符fd传进来了,这个进程的task_struct能够通过自己的PCB找到自己打开的文件列表,然后根据文件描述符fd直接索引对应的数组,找到3号文件描述符里面的内容也就是上图存在fd_array[3]中的结构体地址,找到对应的文件,就可以对文件进行相关操作了。
为什么必须得有文件描述符呢?
当我们打开文件时,操作系统会描述文件的一些相关内容。操作系统要管理文件,必须先打开文件,只有打开文件,才能对文件进行相关操作,打开文件后,操作系统要对文件做管理,就要先描述再组织。
从以上不难看出,文件描述符的本质是内核中的进程和文件关联的数组的下标。
2.文件描述符的分配规则
先看以下代码:
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
int main()
{
int fd = open("./log.txt",O_CREAT | O_WRONLY,0644);
printf("fd = %d\n",fd);
return 0;
}
打印结果:

如果把标准输入关掉呢?那么fd_array数组的下标0就没有使用了,那么上述代码中的fd会变成什么呢?
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
int main()
{
close(0);//把文件描述符0关掉
int fd = op