文章目录
C语言接口
接口函数
操作演示
对文件写入:
#include <stdio.h>
int main()
{
FILE* fp = fopen("log.txt", "w");
if (fp == NULL){
perror("fopen");
return 1;
}
int count = 5;
while (count){
fputs("hello world\n", fp);
count--;
}
fclose(fp);
return 0;
}
运行结果
读取文件:
#include <stdio.h>
int main()
{
FILE* fp = fopen("log.txt", "r");
if (fp == NULL){
perror("fopen");
return 1;
}
char buffer[64];
for (int i = 0; i < 5; i++){
fgets(buffer, sizeof(buffer), fp);
printf("%s", buffer);
}
fclose(fp);
return 0;
}
运行结果:
当前路径
当我们想用fopen以写入的方式打开一个文件时,若该文件不存在,则会自动在当前路径创建该文件,那么这里所说的当前路径指的是什么呢?
查看可执行程序的进程
这里的cwd所指向的路径就是我们所说的当前路径。
当前路径不是指可执行程序所处的路径,而是指该可执行程序运行成为进程时所处的路径。
默认打开的三个流
进程在运行的时默认会打开三个输入输出流,即标准输入流、标准输出流以及标准错误流,对应到C语言当中就是stdin、stdout以及stderr。
其中,标准输入流对应的设备就是键盘,标准输出流和标准错误流对应的设备都是显示器。
查看man手册可以发现它们都是FILE*类型的。
当我们的C程序被运行起来时,操作系统就会默认使用C语言的相关接口将这三个输入输出流打开,之后我们才能调用类似于scanf和printf之类的函数向键盘和显示器进行相应的输入输出操作。
注意: 不止是C语言当中有标准输入流、标准输出流和标准错误流,C++当中也有对应的cin、cout和cerr,其他所有语言当中都有类似的概念。实际上这种特性并不是某种语言所特有的,而是由操作系统所支持的。
系统文件I/O
操作文件除了C语言接口、C++接口或是其他语言的接口外,操作系统也有一套系统接口来进行文件的访问。
相比于C库函数或其他语言的库函数而言,系统调用接口更贴近底层,实际上C/C++等这些语言的库函数都是对系统接口进行了封装。
在Linux平台下运行C代码时,C库函数就是对Linux系统调用接口进行的封装,在Windows平台下运行C代码时,C库函数就是对Windows系统调用接口进行的封装,这样做使得语言有了跨平台性,也方便进行二次开发。
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
open函数原型
int open(const char *pathname, int flags, mode_t mode);
open的参数
open的第一个参数
open函数的第一个参数pathname表示要打开或创建的目标文件。
- 若pathname以路径的方式给出,则当需要创建该文件时,就在pathname路径下进行创建。
- 若pathname以文件名的方式给出,则当需要创建该文件时,默认在当前路径下进行创建。
open的第二个参数
open函数的第二个参数flags表示打开文件的方式。
常用参数如下
open的第三个参数
open函数的第三个参数mode表示创建文件的默认权限。
例:
将mode值设为0666,则创建出来文件的权限为 rw-rw-rw-
实际上创建出来文件的权限值还会受到umask(文件默认掩码)的影响,实际创建出来文件的权限为:mode&(~umask)。umask的默认值一般为0002,当我们设置mode值为0666时实际创建出来文件的权限为0664。
若想创建出来文件的权限值不受umask的影响,则需要在创建文件前使用umask函数将文件默认掩码设置为0。
umask(0);
注意: 当不需要创建文件时,open的第三个参数可以不必设置
open的返回值
open函数的返回值是新打开文件的文件描述符fd。
- 实际上这里所谓的文件描述符本质上是一个指针数组的下标,指针数组当中的每一个指针都指向一个被打开文件的文件信息,通过对应文件的文件描述符就可以找到对应的文件信息。
- 当使用open函数打开文件成功时数组当中的指针个数增加,然后将该指针在数组当中的下标进行返回,而当文件打开失败时直接返回-1,所以,成功打开多个文件时所获得的文件描述符是连续且递增的。
- Linux进程默认情况下会有3个缺省打开的文件描述符,分别是标准输入0、标准输出1、标准错误2
演示
#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);
printf("fd1:%d\n", fd1);
printf("fd2:%d\n", fd2);
printf("fd3:%d\n", fd3);
printf("fd4:%d\n", fd4);
return 0;
}
运行结果:
close
功能: 关闭文件
原型:
int close(int fd);
说明: 使用该函数是只需传入需要关闭文件的文件描述符。关闭文件成功则返回0,关闭文件失败返回-1。
write
功能: 向文件写入信息
原型:
ssize_t write(int fd, const void *buf, size_t count);
说明: 将buf后count字节的数据写入文件描述符为fd的文件中,写入成功返回写入数据的字节个数,写入失败返回-1。
实例:
#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_WRONLY | O_CREAT, 0666);
if (fd < 0){
perror("open");
return 1;
}
const char* msg = "hello world\n";
for (int i = 0; i < 5; i++){
write(fd, msg, strlen(msg));
}
close(fd);
return 0;
}
运行结果:
read
功能: 从文件中读取信息
原型:
ssize_t read(int fd, void *buf, size_t count);
说明: 从文件描述符为fd的文件读取count字节的数据到buf位置当中,读取成功返回读取数据的字节个数,读取失败返回-1。
实例:
#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_RDONLY);
if (fd < 0){
perror("open");
return 1;
}
char ch;
while (1){
ssize_t s = read(fd, &ch, 1);
if (s <= 0){
break;
}
write(1, &ch, 1); //向文件描述符为1的文件写入数据,即向显示器写入数据
}
close(fd);
return 0;
}
运行结果:
文件描述符fd
在进程中每打开一个文件,都会创建有相应的文件描述信息struct_file,这个描述信息被添加在pcb的struct files_struct中,以数组的形式进行管理,随即向用户返回数组的下标作为文件描述符,用于操作文件.
文件是由进程运行时打开的,一个进程可以打开多个文件,系统中又存在大量进程,所以操作系统务必要对这些已经打开的文件进行管理。
操作系统会为每个已经打开的文件创建各自的struct file结构体,然后将这些结构体以双链表的形式连接起来,之后操作系统对文件的管理也就变成了对这张双链表的增删查改等操作。
如何建立进程和文件之间的对应关系:
当一个程序运行起来时,操作系统会将该程序的代码和数据加载到内存,然后为其创建对应的task_struct、mm_struct、页表等相关的数据结构,并通过页表建立虚拟内存和物理内存之间的映射关系,如下图。
文件描述符就是从0开始的小整数。当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件。于是就有了file结构体。表示一个已经打开的文件对象。而进程执行open系统调用,所以必须让进程和文件关联起来。每个进程都有一个指针*files, 指向一张表files_struct,该表最重要的部分就是包涵一个指针数组,每个元素都是一个指向打开文件的指针!所以,本质上,文件描述符就是该数组的下标。所以,只要拿着文件描述符,就可以找到对应的文件,如下图。
注意: 向文件写入数据时,是先将数据写入到对应文件的缓冲区当中,然后定期将缓冲区数据刷新到磁盘当中。
相关问题:
- 为什么创建进程时会默认打开0,1,2 ?
0是标准输入流,对应键盘;1是标准输出流,对应显示器;2是标准错误流,也是对应显示器。通常这3个文件都与终端相联系。我们所用到的从终端输入或输出都不需要打开终端文件。系统自定义了3个文件指针,分别指向标准输入流、标准输出流和标准错误流。
- 磁盘文件和内存文件间的联系
文件存储在磁盘当中时,我们将其称之为磁盘文件,而当磁盘文件被加载到内存当中后,我们将加载到内存当中的文件称之为内存文件。
磁盘文件由两部分构成,分别是文件内容和文件属性。文件内容就是文件当中存储的数据,文件属性就是文件的一些基本信息,例如文件名、文件大小以及文件创建时间等信息都是文件属性,文件属性又被称为元信息。
文件加载到内存时,一般先加载文件的属性信息,当需要对文件内容进行读取、输入或输出等操作时,再加载文件数据。
文件描述符的分配规则
文件描述符是从3开始递增的,因为进程创建时默认打开了0(标准输入流),1(标准输出流),2(标准错误流)。那如果关闭0或2又会有怎样输出呢(这里没有关闭1,因为1是输出流,关闭后就没有显示了)。
关闭0(标准输入流)
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
int main()
{
umask(0);
close(0);// 关闭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);
printf("fd1:%d\n", fd1);
printf("fd2:%d\n", fd2);
printf("fd3:%d\n", fd3);
return 0;
}
运行结果:
再关闭2(标准错误流)运行结果又如何呢?
结论: 文件描述符是从最小且没有被使用的fd_array数组下标开始进行分配的。
重定向
重定向的原理
每个文件描述符都是一个内核中文件描述信息数组的下标,对应有一个文件的描述信息用于操作文件,而重定向就是在不改变所操作的文件描述符的情况下,通过改变描述符对应的文件描述信息进而实现改变所操作的文件
演示:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
int main()
{
close(1);
int fd = open("myfile", O_WRONLY|O_CREAT, 00644);
if(fd < 0){
perror("open");
return 1;
}
printf("fd: %d\n", fd);
fflush(stdout);
close(fd);
return 0;
}
运行一下我们发现,本来应该输出到显示器上的内容,输出到了文件 myfile 当中,其中,fd=1。这种现象叫做输出重定向。常见的重定向有:>(输出重定向), >>(追加重定向), <(输入重定向)。
重定向的本质如图所示:
dup2
函数原型:
int dup2(int oldfd, int newfd);
功能: dup2将fd_array[oldfd]的内容拷贝到fd_array[newfd]当中,如果有必要的话需要先使用关闭文件描述符为newfd的文件。
返回值: dup2如果调用成功,返回newfd,否则返回-1。
使用dup2时,我们需要注意以下两点:
- 如果oldfd不是有效的文件描述符,则dup2调用失败,并且此时文件描述符为newfd的文件没有被关闭。
- 如果oldfd是一个有效的文件描述符,但是newfd和oldfd具有相同的值,则dup2不做任何操作,并返回newfd。
使用实例:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
int main()
{
int fd = open("log.txt", O_WRONLY | O_CREAT, 0666);
if (fd < 0){
perror("open");
return 1;
}
close(1);
dup2(fd, 1);
printf("hello printf\n");
fprintf(stdout, "hello fprintf\n");
return 0;
}
运行结果:
数据被输出到了log.txt文件中
上文中的 >(输出重定向)等同于dup2(fd,1), >>(追加重定向)等同于dup2(fd,1)文件的写入方式不同, <(输入重定向)等同于dup2(fd,0)。
FILE
FILE当中的文件描述符
因为库函数是对系统调用接口的封装,本质上访问文件都是通过文件描述符fd进行访问的,所以C库当中的FILE结构体内部必定封装了文件描述符fd。
我们可以在/usr/include/stdio.h中找到FILE结构体源码:
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
};
FILE当中的缓冲区
看下面代码
#include <stdio.h>
#include <unistd.h>
int main()
{
//c
printf("hello printf\n");
fputs("hello fputs\n", stdout);
//system
write(1, "hello write\n", 12);
fork();
return 0;
}
运行结果:
将程序结果重定向到log.txt文件中
我们发现C库函数打印的内容重定向到文件后就变成了两份,系统接口打印的内容还是原来的一份,这是为什呢?
当我们执行可执行程序,将数据打印到显示器时所采用的是行缓冲,因为代码中的\n将数据刷新到了显示器上。
当我们将运行结果重定向到log.txt文件时,数据的刷新策略就变为了全缓冲,此时我们使用printf和fputs函数打印的数据都打印到了C语言自带的缓冲区当中,之后当我们使用fork函数创建子进程时,由于进程间具有独立性,而之后当父进程或是子进程对要刷新缓冲区内容时,本质就是对父子进程共享的数据进行了修改,此时就需要对数据进行写时拷贝,至此缓冲区当中的数据就变成了两份,一份父进程的,一份子进程的,所以重定向到log.txt文件当中printf和puts函数打印的数据就有两份。但由于write函数是系统接口,我们可以将write函数看作是没有缓冲区的,因此write函数打印的数据就只打印了一份。
综上: printf fwrite 库函数会自带缓冲区,而 write 系统调用没有带缓冲区。另外,我们这里所说的缓冲区,都是用户级缓冲区。
缓冲区谁提供呢?
printf fwrite 是库函数, write 是系统调用,库函数在系统调用的“上层”, 是对系统调用的“封装”,但是 write 没有缓冲区,而 printf fwrite 有,足以说明,该缓冲区是二次加上的,是C标准库提供。
理解文件系统
文件分为内存文件和磁盘文件,上文讲的是内存文件,下面讲磁盘文件
初识inode
磁盘文件由文件内容和文件属性两部分组成。文件内容是文件中存储的数据信息,文件属性是文件的一些信息,如文件名,文件大小,修改时间等。文件属性又被称为元信息。
使用 ls -l 指令可以显示当前目录下的文件属性
信息对应的文件属性如下
文件的属性和内容是分离存储的,保存文件属性的结构称为inode,每个inode都有自己的inode编号。
我们可以用 ls -li 查看当前目录下各文件的inode编号
磁盘的概念
对磁盘进行读写操作时,一般有以下几个步骤:
- 确定读写信息在磁盘的哪个盘面。
- 确定读写信息在磁盘的哪个柱面。
- 确定读写信息在磁盘的哪个扇区。
磁盘分区与格式化介绍
磁盘分区
磁盘通常被称为块设备,一般以扇区为单位,一个扇区的大小通常为512字节。
计算机为了更好的管理磁盘,于是对磁盘进行了分区。磁盘分区就是使用分区编辑器在磁盘上划分几个逻辑部分,盘片一旦划分成数个分区,不同的目录与文件就可以存储进不同的分区,分区越多,就可以将文件的性质区分得越细,按照更为细分的性质,存储在不同的地方以管理文件,例如在Windows下磁盘一般被分为C盘和D盘两个区域。
磁盘格式化
磁盘格式化就是对分区后的各个区域写入对应的管理信息。
写入的管理信息是什么是由文件系统决定的,不同的文件系统格式化时写入的管理信息是不同的,常见的文件系统有EXT2、EXT3、XFS、NTFS等。
EXT2文件系统的存储方案
计算机为了更好的管理磁盘,会对磁盘进行分区。而对于每一个分区来说,分区的头部会包括一个启动块(Boot Block),对于该分区的其余区域,EXT2文件系统会根据分区的大小将其划分为一个个的块组(Block Group)。
启动块的大小是确定的,而块组的大小是由格式化的时候确定的,并且不可以更改。
每个块组都有相同的结构,结构如下
- 超级块(Super Block):存放文件系统本身的结构信息。记录的信息主要有:block 和 inode的总量,未使用的block和inode的数量,一个block和inode的大小,最近一次挂载的时间,最近一次写入数据的时间,最近一次检验磁盘的时间等其他文件系统的相关信息。Super Block的信息被破坏,可以说整个文件系统结构就被破坏了。
- GDT(Group Descriptor Table):块组描述符,描述块组属性信息。
- 块位图(Block Bitmap):Block Bitmap中记录着Data Block中哪个数据块已经被占用,哪个数据块没有被占用。
- inode位图(inode Bitmap):每个bit表示一个inode是否空闲可用。
- i节点表(inode Table):存放文件属性 如 文件大小,所有者,最近修改时间等.
- 数据区(Data Blocks):存放文件内容
相关问题
如何创建一个文件?
- 存储属性
内核先找到一个空闲的i节点(这里是263466)。内核把文件信息记录到其中
- 存储数据
该文件需要存储在三个磁盘块,内核找到了三个空闲块:300,500,800。将内核缓冲区的第一块数据复制到300,下一块复制到500,以此类推。
- 记录分配情况
文件内容按顺序300,500,800存放。内核在inode上的磁盘分布区记录了上述块列表。
- 添加文件名到目录
新的文件名abc。linux如何在当前的目录中记录这个文件?内核将入口(263466,abc)添加到目录文件。文件名和inode之间的对应关系将文件名和文件的内容及属性连接起来。
说明:
一个文件使用的数据块和inode结构的对应关系,是通过一个数组进行维护的,该数组一般可以存储15个元素,其中前12个元素分别对应该文件使用的12个数据块,剩余的三个元素分别是一级索引、二级索引和三级索引,当该文件使用数据块的个数超过12个时,可以用这三个索引进行数据块扩充。
为什么拷贝文件的时候很慢,而删除文件的时候很快?
因为拷贝文件需要先创建文件,然后再对该文件进行写入操作,该过程需要先申请inode号并填入文件的属性信息,之后还需要再申请数据块号,最后才能进行文件内容的数据拷贝,而删除文件只需将对应文件的inode号和数据块号置为无效即可(当inode号和数据块号为无效时,创建新文件时会将该inode号和数据块号重新分配出去,将已失效的inode号和数据块号覆盖,从而达到删除的效果)。
如何理解目录是文件
- 目录有自己的属性信息,目录的inode结构当中存储的就是目录的属性信息,比如目录的大小、目录的拥有者等。
- 目录也有自己的内容,目录的数据块当中存储的就是该目录下的文件名以及对应文件的inode指针。
注意: 每个文件的文件名并没有存储在自己的inode结构当中,而是存储在该文件所处目录文件的文件内容当中。因为计算机并不关注文件的文件名,计算机只关注文件的inode号,而文件名和文件的inode指针存储在其目录文件的文件内容当中后,目录通过文件名和文件的inode指针即可将文件名和文件内容及其属性连接起来。
软硬链接
软链接
创建一个文件的软连接。
文件的软连接和原文件inode号是不同的,而且软连接比原文件小的多
为什么软连接比原文件小的多呢?
软链接又叫做符号链接,软链接文件相对于源文件来说是一个独立的文件,该文件有自己的inode号,但是该文件只包含了源文件的路径名,类似于Windows操作系统当中的快捷方式。
注意: 软链接文件只是其原文件的一个标记,当删除了原文件后,链接文件不能独立存在,虽然仍保留文件名,但不能执行或是查看软链接的内容。
硬链接
创建一个文件的硬连接。
硬链接文件的inode号与原文件的inode号是相同的,且硬链接文件和原文件的属性是相同的,硬链接数由原来的1变为了2。
硬链接文件本质上就是原文件的一个别名,类似C++引用,当其中一个文件被修改后另一个文件也会跟者改动。硬链接数就是该原文件有多少个硬链接文件。
看如下操作,创建一个目录list
我们发现刚创建目录的硬连接数为2,这时为什么呢?
因为每个目录创建后,该目录下默认会有两个隐含文件 . 和 . . ,它们分别代表当前目录和上级目录,因此这里创建的目录有两个名字,一个是list另一个就是list目录下的 . ,所以刚创建的目录硬链接数是2。
软硬链接的区别
- 软链接是一个独立的文件,有独立的inode,而硬链接没有独立的inode。
- 软链接相当于快捷方式,硬链接本质没有创建文件,只是建立了一个文件名和已有的inode的映射关系,并写入当前目录,方便相对路径的设置。
文件的三个时间
在Linux中我们可以通过 stat+文件名 来查看文件信息
这里的文件包含了三个时间信息
- Access: 文件最后被访问的时间。
- Modify: 文件内容最后的修改时间。
- Change: 文件属性最后的修改时间。
当我们修改文件内容时,文件的大小一般也会随之改变,所以一般情况下Modify的改变会带动Change一起改变,但修改文件属性一般不会影响到文件内容,所以一般情况下Change的改变不会带动Modify的改变。
当文件进行编译时,会比较生成的可执行文件和需要编译文件的最后修改时间,如果前者时间晚于后者就不在编译,如下图所示。
这样在编译多个文件时,已编译的文件不再编译,节省了时间。
若想将文件的时间更新到最新状态,可以使用 touch 指令
注意: touch+文件 指令如果文件不存在,就会创建新文件,如果文件存在就会更新问文件时间。