C语言中的文件操作函数如下:
文件操作函数 | 功能 |
|---|---|
fopen | 打开文件 |
fclose | 关闭文件 |
fputc | 写入一个字符 |
fgetc | 读取一个字符 |
fputs | 写入一个字符串 |
fgets | 读取一个字符串 |
fprintf | 格式化写入数据 |
fscanf | 格式化读取数据 |
fwrite | 向二进制文件写入数据 |
fread | 从二进制文件读取数据 |
fseek | 设置文件指针的位置 |
ftell | 计算当前文件指针相对于起始位置的偏移量 |
rewind | 设置文件指针到文件的起始位置 |
ferror | 判断文件操作过程中是否发生错误 |
feof | 判断文件指针是否读取到文件末尾 |
下面只会选择性对C语言的部分文件操作函数进行使用,若想详细了解其余文件操作函数的使用方法,请跳转到博主的其它博客:文件处理不再难:带你轻松攻克C语言文件操作_c语言大文件处理-CSDN博客
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
int main()
{
FILE * fp = fopen("log.txt","w");
if(NULL == fp)
{
perror("fopen");
exit(-1);
}
fclose(fp);
fp = NULL;
return 0;
}
运行程序后,在当前路径下就会生成对应文件.

那么我们也可以通过输出重定向向文件里面写入新的内容.


那么有了上面的简单例子我们可以对文件有一个简单的初步理解
系统中可以存在很多进程------------>很多情况下,OS内部,一定存在大量的被打开的文件---------->那么OS就要对这些被打开的文件进行处理,先描述,再组织------->因此可以推测,每一个被打开的文件在OS内部,一定要存在对应的描述文件属性的结构体,类似与PCB.


我们都知道在Linux下一切皆文件,也就是说Linux下的任何东西都可以看作是文件,那么显示器和键盘当然也可以看作是文件.我们能看到显示器上面的相关数据,是因为我们向"显示器文件”写入了数据,电脑能获取到我们敲击键盘时对应的字符,是因为电脑从"键盘文件"读取了数据.
有的uu就会有疑问,为什么我们向“显示器文件”写入数据以及从“键盘文件”读取数据前,不需要进行打开“显示器文件”和“键盘文件”的相应操作?

查看man手册我们就可以发现,stdin、stdout以及stderr这三个家伙实际上都是
FILE*类型的。

那么可以试想一个场景,当我们使用fputs函数时,将第二个参数设置为stdout,此时fputs函数会不会将数据显示在显示器上呢.
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
int main()
{
fputs("hello stdin\n",stdout);
fputs("hello stdout\n",stdout);
fputs("hello stderr\n",stdout);
}
不止是C语言当中有标准输入流、标准输出流和标准错误流,C++当中也有对应的cin、cout和cerr,其他所有语言当中都有类似的概念。实际上这种特性并不是某种语言所特有的,而是由操作系统所支持的。
操作文件,除了上述 C 接口(当然, C++ 也有接口,其他语言也有),我们还可以采用系统接口来进行文件访问.

我们在Linux平台下运行C代码时,C库函数就是对Linux系统调用接口进行的封装,在Windows平台下运行C代码时,C库函数就是对Windows系统调用接口进行的封装,这样做使得语言有了跨平台性,也方便进行二次开发.

open函数的第一个参数是pathname,表示要打开或创建的目标文件.
open函数的第二个参数是flags,表示打开文件的方式。
参数选项 | 含义 |
|---|---|
O_RDONLY | 以只读的方式打开 |
O_WRONLY | 以只写的方式打开文件 |
O_APPEND | 以追加的方式打开 |
O_RDWR | 以读写的方式打开 |
O_CREAT | 当目标文件不存在时,创建文件 |
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <fcntl.h>
int main()
{
int fd =open("test.txt",O_WRONLY | O_CREAT);
if(fd < 0 )
{
perror("open");
exit(-1);
}
return 0;
}
相信通过上面的代码uu们对open函数有了一个基础的了解,但是会存下以下两个疑问

这些宏定义选项的共同点就是,它们的二进制序列当中有且只有一个比特位是1(O_RDONLY选项的二进制序列为全0,表示O_RDONLY选项为默认选项),且为1的比特位是各不相同的,这样一来,在open函数内部就可以通过使用“与”运算来判断是否设置了某一选项。
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <fcntl.h>
//0000 0001
#define One 1
//0000 0010
#define Two (1 << 1)
//0000 0100
#define Three (1 << 2)
//0000 1000
#define Four (1 << 3)
void Print(int Flag)
{
if (Flag & One)
printf("Function One\n");
if (Flag & Two)
printf("Function Two\n");
if (Flag & Three)
printf("Function Three\n");
if (Flag & Four)
printf("Function Four\n");
}
int main()
{
Print(One);
printf("\n");
Print(One | Two);
printf("\n");
Print(One | Two | Three);
printf("\n");
Print(One | Four);
printf("\n");
return 0;
}
创建出来的文件具有的权限跟正常创建出来的文件所具有的权限不一样,这就要牵扯到open函数的第三个参数了
open函数的第三个参数是mode,表示创建文件的默认权限。
例如,将mode设置为0666,则文件创建出来的权限如下:

但实际上创建出来文件的权限值还会受到umask(文件默认掩码)的影响,实际创建出来文件的权限为:mode&(~umask)。umask的默认值一般为0002,当我们设置mode值为0666时实际创建出来文件的权限为0664。


若想创建出来文件的权限值不受umask的影响,则需要在创建文件前使用**
umask**函数将文件默认掩码设置为0。
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <fcntl.h>
int main()
{
//将umask码设置成0
umask(0);
int fd = open("test.txt",O_WRONLY|O_CREAT,0666);
if(fd < 0)
{
perror("open");
exit(-1);
}
return 0;
}
open函数的返回值是新打开文件的文件描述符。
我们可以尝试一次打开多个文件,然后分别打印它们的文件描述符。
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
int main()
{
umask(0);
int fd1 = open("log1.txt", O_RDONLY | O_CREAT, 0666);
int fd2 = open("log2.txt", O_RDONLY | O_CREAT, 0666);
int fd3 = open("log3.txt", O_RDONLY | O_CREAT, 0666);
int fd4 = open("log4.txt", O_RDONLY | O_CREAT, 0666);
int fd5 = open("log5.txt", O_RDONLY | O_CREAT, 0666);
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;
}
运行程序后可以看到,打开文件的文件描述符是从3开始连续且递增的,那么为什么是从3开始递增的呢

我们再尝试打开一个根本不存在的文件,也就是open函数打开文件失败。
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
int main()
{
int fd = open("test.txt",O_RDONLY);
printf("%d\n",fd);
return 0;
}

使用close函数时传入需要关闭文件的文件描述符即可,若关闭文件成功则返回0,若关闭文件失败则返回-1。
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
int fd = open("test.txt",O_WRONLY | O_CREAT,0666);
if(fd < 0)
{
perror("open");
return 1;
}
printf("%d\n",fd);
close(fd);
return 0;
}
系统接口中使用write函数向文件写入信息,write函数的函数原型如下:
ssize_t write(int fd, const void *buf, size_t count);使用write函数,将buf位置开始向后count字节的数据写入文件描述符为fd的文件中.
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
int main()
{
//O_WRONLY表示只写打开,O_CREAT表示若文件不存在,则创建,并且要提供对应的权限码
int fd = open("test.txt",O_WRONLY | O_CREAT,0666);
if(fd < 0)
{
perror("open");
return 1;
}
const char * message = "cwd world";
write(fd,message,strlen(message));
close(fd);
return 0;
}
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
int main()
{
//写方式打开,不存在文件则创建文件,存在文件的话会先刷新文件的内容
int fd = open("test.txt",O_WRONLY | O_CREAT | O_TRUNC,0666);
if(fd < 0)
{
perror("open");
return 1;
}
const char * message = "hello Linux";
write(fd,message,strlen(message));
close(fd);
return 0;
}#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
int main()
{
int fd = open("test.txt",O_RDONLY);
if(fd < 0)
{
perror("open");
return 1;
}
char ch;
while (1)
{
//从文件描述符为fd的文件读取1个字节的数据到Message中
ssize_t Message = read(fd,&ch,1);
if(Message <=0)
break;
printf("%c",ch);
}
printf("\n");
close(fd);
return 0;
}系统接口中使用read函数从文件读取信息,read函数的函数原型如下.
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
int main()
{
int fd = open("test.txt",O_RDONLY);
if(fd < 0)
{
perror("open");
return 1;
}
char ch;
while (1)
{
//从文件描述符为fd的文件读取1个字节的数据到Message中
ssize_t Message = read(fd,&ch,1);
if(Message <=0)
break;
printf("%c",ch);
}
printf("\n");
close(fd);
return 0;
}
我们可以使用read函数,从文件描述符为fd的文件读取count字节的数据到buf位置当中。
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
int main()
{
int fd = open("test.txt",O_RDONLY);
if(fd < 0)
{
perror("open");
return 1;
}
char ch;
while (1)
{
//从文件描述符为fd的文件读取1个字节的数据到Message中
ssize_t Message = read(fd,&ch,1);
if(Message <=0)
break;
printf("%c",ch);
}
printf("\n");
close(fd);
return 0;
}
进程和文件之间的关系是如何建立的. 我们知道当一个程序运行起来的时候,操作系统会将该程序的代码和数据加载到内存当中,然后创建对应的task_struct与mm_struct、页表等相关的数据结构,并通过页表建立虚拟内存与物理内存之间的对应关系.

而task_struct当中有一个指针,该指针指向一个名为files_struct的结构体,在该结构体当中就有一个名为fd_array的指针数组,该数组的下标就是我们所谓的文件描述符,因此一个进程想要找到对应的文件,只需要在对应在指针数组struct file * fdN中的下标,将其返回给上层,就能够访问文件,因此文件描述符fd的本质是:内核的进程:文件映射关系的数组的下标.

什么叫做进程创建的时候会默认打开0、1、2?
磁盘文件VS内存文件.
Linux一切皆文件的原因.

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
int main()
{
int fd = open("test.txt",O_RDONLY);
if(fd < 0)
{
perror("open");
return 1;
}
printf("fd:> %d\n",fd);
close(fd);
return 0;
}
输出发现是 fd: 3,关闭0或者2,再看
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
int main()
{
close(0);
int fd = open("test.txt",O_RDONLY);
if(fd < 0)
{
perror("open");
return 1;
}
printf("fd:> %d\n",fd);
close(fd);
return 0;
}
发现是结果是:fd:0,文件描述符的分配规则:在files_struct数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符.
我们来看一段代码
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
const char * FileName = "log.txt";
int main()
{
umask(0);
close(1);
int fd = open(FileName,O_CREAT | O_WRONLY | O_TRUNC,0666);
if(fd < 0)
{
perror("open");
return 1;
}
printf("fd:> %d\n",fd);
fprintf(stdout,"fprintf,fd:>%d\n",fd);
fflush(stdout);
close(fd);
return 0;
}
我们可以看到此时显示器上并没有输出数据,对应数据输出到了log.txt文件当中,那么这是为什么呢?
printf与fprintf,他们只认stdout,但stdout里头封装的文件描述符依旧是1,但在内核当中,1号下标已经不是指向之前的显示器了,而是指向一个新打开的文件了,printf与fprintf默认打印的时候,本质就是往显示器上打印,本应该打印到显示器上的内容,却打印到了log.txt里头,这种技术就叫做重定向.

为什么log.txt的文件描述符会变成1呢
根据文件描述符的分配规则,由于创建了文件并关闭了1号下标,所以导致1号下标并为被使用,因此原本1号下标指向标准输出(显示器)最终成为了log.txt的文件描述符.
那么因此我们可以知道,重定向的本质:是在内核中改变文件描述符表特定下标的内容,与上层无关.

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
const char * FileName = "log.txt";
int main()
{
umask(0);
close(1);
int fd = open(FileName,O_CREAT | O_WRONLY | O_TRUNC,0666);
if(fd < 0)
{
perror("open");
return 1;
}
printf("fd:> %d\n",fd);
fprintf(stdout,"fprintf,fd:>%d\n",fd);
//fflush(stdout);
close(fd);
return 0;
}
我们可以看到,当将fflush这段代码屏蔽掉了以后,此时再运行程序,会发现log.txt啥都没有,那么这是为什么呢

要完成重定向我们只需进行fd_array数组当中元素的拷贝即可。例如,我们若是将fd_array3当中的内容拷贝到fd_array1当中,因为C语言当中的stdout就是向文件描述符为1文件输出数据,那么此时我们就将输出重定向到了文件log.txt。

在Linux操作系统中提供了系统接口dup2,我们可以使用该函数完成重定向。dup2的函数原型如下:

使用dpu2,我们需要注意以下两点
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
const char * FileName = "log.txt";
int main()
{
int fd = open(FileName,O_CREAT | O_WRONLY | O_TRUNC,0666);
//fd指向的文件描述符复制到文件描述符1,即让1成为fd的一个拷贝,使得两者指向相同的文件即log.txt
dup2(fd,1);
printf("hello world\n");
fprintf(stdout,"hello Linux\n");
return 0;
}
将打开文件log.txt时获取到的文件描述符和1传入dup2函数,那么dup2将会把fd_arryafd的内容拷贝到fd_array1中,在代码中我们向stdout输出数据,而stdout是向文件描述符为1的文件输出数据,因此,本应该输出到显示器的数据就会重定向输出到log.txt文件当中。
首先,我们在**
/usr/include/stdio.h**头文件中可以看到下面这句代码,也就是说FILE实际上就是**struct _IO_FILE**结构体的一个别名。
typedef struct _IO_FILE FILE;而我们在**
/usr/include/libio.h**头文件中可以找到**struct _IO_FILE**结构体的定义,在该结构体的众多成员当中,我们可以看到一个名为**_fileno**的成员,这个成员实际上就是封装的文件描述符。
在/usr/include/libio.h
struct _IO_FILE {
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags
//缓冲区相关
/* 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. */
struct _IO_marker *_markers;
struct _IO_FILE *_chain;
int _fileno; //封装的文件描述符
#if 0
int _blksize;
#else
int _flags2;
#endif
_IO_off_t _old_offset; /* This used to be _offset but it's too small. */
#define __HAVE_COLUMN /* temporary */
/* 1+column number of pbase(); 0 is unknown. */
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];
/* char* _save_gptr; char* _save_egptr; */
_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};现在再来理解一下C语言当中的fopen函数究竟在做什么?
什么是缓冲区.
缓冲区就是一段内存空间,由语言维护就叫做语言级的缓冲区,由操作系统内核维护就叫做内核级的缓冲区.
为什么存在缓冲区.
给上层提供良好的IO体验,间接提高整体的效率.
缓冲区的分类以及作用
缓冲区主要分为用户级缓冲区与内核级缓冲区,二者的作用在于
缓冲区的刷新策略.
缓冲区刷新的特殊情况.

我们来看一段代码.
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
int main()
{
const char * message0 = "hello printf\n";
const char * message1 = "hello fwrite\n";
const char * message2 = "hello write\n";
printf("%s",message0);
fwrite(message1,strlen(message1),1,stdout);
write(1,message2,strlen(message2));
fork();
return 0;
}
通过观察我们可以发现,对进程实现了输出重定向之后,printf和fwrite函数都输出了两次,而write这个系统调用函数只输出了一次.为什么呢?
我们知道文件可以分为磁盘文件和内存文件,内存文件前面我们已经谈过了,接下来谈谈磁盘文件。
磁盘文件由两部分组成,分别是文件内容和文件属性.文件内容就是文件当中存储的数据,文件属性就是文件的一些基本信息,例如文件名、文件大小以及文件创建时间等信息都是文件属性,文件属性又被称为元信息.
在命令行当中输入**ls -l**,即可显示当前目录下各文件的属性信息。

其中,各列信息所对应的文件属性如下:

在命令行当中输入**ls -i**,即可显示当前目录下各文件的inode编号。

PS:无论是文件内容还是文件属性,都是存储在Inode当中的.
什么是磁盘?

磁盘的基本概念.

磁盘的寻找方案.
对磁盘进行读写操作时,一般有以下几个步骤:
通过上面的三个步骤,最终确定信息在磁盘的读写位置.
理解文件系统,首先我们需要将磁盘想象成一个线性的存储介质,联想一下磁带,当磁带被卷起来时,其就像磁盘一样是原形的,但当我们把磁带拉直以后,其就是线性的.

磁盘通常被称作为块设备,一般以扇区为单位,一个扇区的大小通常为512字节,我们若以大小为512GB的磁盘为例,该磁盘可被分为10亿多个扇区.

计算机为了更好的管理磁盘,于是对磁盘进行了分区。磁盘分区就是使用分区编辑器在磁盘上划分几个逻辑部分,盘片一旦划分成数个分区,不同的目录与文件就可以存储进不同的分区,分区越多,就可以将文件的性质区分得越细,按照更为细分的性质,存储在不同的地方以管理文件,例如在Windows下磁盘一般被分为C盘和D盘两个区域。
在Linux操作系统中,我们也可以通过以下命令查看我们磁盘的分区信息:

磁盘格式化

其中,写入的管理信息是什么是由文件系统决定的,不同的文件系统格式化时写入的管理信息是不同的,常见的文件系统有EXT2、EXT3、XFS、NTFS等.
计算机为了更好的管理磁盘,会对磁盘进行分区。而对于每一个分区来说,分区的头部会包括一个启动块(Boot Block),对于该分区的其余区域,EXT2文件系统会根据分区的大小将其划分为一个个的块组(Block Group)。

PS:启动块的大小是确定的,而块组的大小是由格式化的时候确定的,并且不可以更改。
其次,每个组块都有相同的组成结构,每个组块都由超级块(Super Block)、块组描述符表(Group Descriptor Table)、块位图(Block Bitmap)、Inode位图(inode Bitmap)、inode表(inode Table)以及数据表(Data Block)组成.

PS:
将属性和数据分开存放的想法看起来很简单,但实际上是如何工作的呢?我们通过 touch 一个新文件来看看如何工作。

为了说明问题,我们将上图简化:

创建一个新文件主要有以下4个操作.
如何理解对文件写入信息.
PS:一个文件使用的数据块和inode结构的对应关系,是通过一个数组进行维护的,该数组一般可以存储15个元素,其中前12个元素分别对应该文件使用的12个数据块,剩余的三个元素分别是一级索引、二级索引和三级索引,当该文件使用数据块的个数超过12个时,可以用这三个索引进行数据块扩充。 如何理解删除一个文件
由于此操作并不会真正将文件对应的信息删除,而只是将其inode号和数据块号置为了无效,所以当我们删除文件后短时间内是可以恢复的。为什么说是短时间内呢,因为该文件对应的inode号和数据块号已经被置为了无效,因此后续创建其他文件或是对其他文件进行写入操作申请inode号和数据块号时,可能会将该置为无效了的inode号和数据块号分配出去,此时删除文件的数据就会被覆盖,也就无法恢复文件了. 为什么拷贝文件很慢,而删除文件却很快.
PS:每个文件的文件名并没有存储在自己的inode结构当中,而是存储在该文件所处目录文件的文件内容当中。因为计算机并不关注文件的文件名,计算机只关注文件的inode号,而文件名和文件的inode指针存储在其目录文件的文件内容当中后,目录通过文件名和文件的inode指针即可将文件名和文件内容及其属性连接起来。
那么有了目录以及目录项的概念之后,那么我们其中便可以引入软链接的概念
那么我们在解释其原理之前,我们先来看看现象.
ln -s [目标文件或目录] [软链接名称]

我们可以看到,./run.link所执行出来的效果跟ls指令一样.那么这是怎么做到的呢?
PS:软链接指向的目标文件一定要存在,如果不存在,那么软链接保存的内容是无效的,那么此时该软链接的状态就是悬浮的.
那么在我们Windows下我们的一个可执行程序的成功执行需要编译各种配置的源文件,那么可执行文件和其源文件会封装到一个文件夹在特定的盘的特定路径下保存,那么我们运行这个可执行文件,那么我们就得知道其路径,但是我们用户通常不需要记住每一个可执行文件它保存的路径在哪里,而是通过桌面的快捷方式点击即可运行,那么快捷方式本质上就是一个软链接,那么它内部记录了可执行文件的路径,那么打开该快捷方式其实本质上就是解析其路径然后运行目标的可执行文件即可.
那么有软连接,我们Linux还有硬链接的存在,那么它的作用其实和软链接是差不多的.
ln 目标文件名 硬链接那么我们在解释其硬链接的原理之前,我们先来看看现象.

输入ls -li来查看该该目录下的文件的inode编号,我们发现硬链接文件以及指向的目标文件的inode编号是相同的,那么因此可以说明硬链接不是一个独立的文件,因为没有独立的inode number,使用的是目标文件的inode number.

我们可以看到,当使用cat命令打印硬链接的文件的内容时,硬链接打印的内容是指向目标文件的File_Target.txt的文件内容,那么这是怎么做到的呢?


