在学习C语言时我们了解了一些C语言的对于文件操作的接口。
其中有fopen、fclose、fputc、fgetc、fputs、fgets、fprintf、fscanf、fread、fwrite等
用一段代码简单回顾一下:
#include
#include
int main()
{
FILE* fp=fopen("myfile","a+");
if(!fp)
{
printf("fopen error\n");
}
int count=5;
const char *m="hello linux\n";
while(count--){
//对文件进行写入
fwrite(m,strlen(m),1,fp);
}
//关闭文件
fclose(fp);
return 0;
}
我们先要了解,把内容写入文件中,先要有这个文件,然后就是要打开这个文件。
在上面的代码中
FILE* fp=fopen("myfile","a+");
第一个参数:文件的路径/文件名(不带路径会在当前路径下创建这个文件)。
当前路径:当前进程运行的路径。
第二个参数:就是以怎样的方式来。
在学习Linux时,我们经常听说“一切皆文件“。
那么显示器、键盘是文件吗?
在C语言时,我们经常用printf()函数来把内容显示到显示器上。
而现在,我们不用printf()函数来打印内容。
#include
#include
int main()
{
char *m="hello linux\n";
fwrite(m,strlen(m),1,stdout);
return 0;
}
这样我们可以了解,显示器也可以看作文件,也可以用fwrite()函数来写入。
重点来了:任何进程在运行时,都会默认打开三个输入输出流。
分别是:
标准输入(键盘)stdin
标准输出(显示器)stdin
标准错误(显示器)stderr
这三个流的类型都是FILE*,文件指针。
文件操作除了上述的C接口以外,还有我们的系统接口来进行对文件的操作。
我们用C接口操作文件在Linux上跑,其实是C在调用Linux的系统接口来完成的。
所以说,C库文件的接口是对系统调用接口的一次封装。
第一个接口:open接口,与C的区别是前面没有f。
#include
#include
#include
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
参数分别是:路径或文件名、选项、权限。
选项:
其中注意的是这些接口的返回类型是int。
文件打开成功后,会返回一个较小的非负整数,表示该文件的文件描述符。
失败返回-1。
用一段代码来感受一下吧。
#include
#include
#include
#include
#include
#include
int main()
{
umask(0);
int fd=open("myfile",O_WRONLY|O_CREAT,0666);
if(fd<0)
{
perror("open");
return 1;
}
char *buf="hello linux\n";
write(fd,buf,strlen(buf));
close(fd);
return 0;
}
其中一: open中的0666,表示创建文件的时候文件权限的666。当然这要设置一下默认掩码。
其中二:O_WRONLY|O_CREAT 表示如果有该文件就对该文件以只写的方式打开,如果没有就创建这个文件,权限为666,以只写的方式打开。
为什么要用O_WRONLY|O_CREAT来表示呢?
不难看出,这些用大写字母来表示的选项是用宏。这些宏都是对应一个bit位,像位图一样。
我们在传O_WRONLY|O_CREAT的时候,
会用if(O_WRONLY&F)来判断这个选项等等。
而在write中第一个参数是文件的描述符。而文件描述符又是什么呢?
我们先用一段代码来感受文件描述符:
#include
#include
#include
#include
int main()
{
umask(0);
int fd1=open("myfile",O_WRONLY|O_CREAT,0666);
int fd2=open("myfile",O_WRONLY|O_CREAT,0666);
int fd3=open("myfile",O_WRONLY|O_CREAT,0666);
int fd4=open("myfile",O_WRONLY|O_CREAT,0666);
printf("fd1:%d\n",fd1);
printf("fd2:%d\n",fd2);
printf("fd3:%d\n",fd3);
printf("fd4:%d\n",fd4);
return 0;
}
open执行成功返回一个较小的非负整形,也就是文件描述符。
通过上面的代码执行效果来看,有点像一个数组的下标。
其实这就是一个数组的下标,其中数组的0、1、2下标分被键盘(标准输入)、显示器(标准输出)、显示器(标准错误)给占了。
所以分配下来的是3、4、5、6。
而为什么是数组呢?
那么我们先要了解内存文件和磁盘文件了。
上面的代码创建了myfile文件,其文件的属性(文件大小、文件名、最近一次修改文件的时间等)会以struct file结构体在内存中保存起来。
而该文件的内容是在磁盘上的,也就是磁盘文件。
而这些struct file会被操作系统用双链表的形式来组织起来,和PCB类似。
而一个进程创建会创建PCB,其中PCB中有一个指针指向一个叫files_struct的结构体,其结构体中有一部分是以指针数组的形式存在的,其中存放的内容就是struct file结构体的地址。
进程通过文件描述符找到这个存放文件地址的地放,进而来对文件进行操作。
而现在知道,文件描述符就是从0开始的小整数。当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件。
于是就有了file结构体。表示一个已经打开的文件对象。而进程执行open系统调用,所以必须让进
程和文件关联起来。每个进程都有一个指针*files, 指向一张表files_struct,该表最重要的部分就是包涵一个指针数组,每个元素都是一个指向打开文件的指针!所以,本质上,文件描述符就是该数组的下标。所以,只要拿着文件描述符,就可以找到对应的文件
如果我们close(0)
#include
#include
#include
#include
#include
int main()
{
close(0);
umask(0);
int fd1=open("myfile",O_WRONLY|O_CREAT,0666);
printf("fd1:%d\n",fd1);
return 0;
}
文件描述符的分配规则:在files_struct数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符。
我们如果关闭close(1)
//输出重定向
#include
#include
#include
#include
#include
#include
int main()
{
close(1);
umask(0);
int fd1=open("myfile",O_WRONLY|O_CREAT,0666);
char *duf="hello linux\n";
//printf("%s",duf);
//fflush(stdout);//更新流的用户空间缓冲数据
write(1,duf,strlen(duf));
return 0;
}
在学习C语言中的对文件操作的函数中有FILE*类型的。
FILE *fopen(const char *path, const char *mode);
那么FILE*是什么呢?
FILE是一个结构体,FILE*是一个结构体的指针。
我们都知道,C库中的IO相关的函数其实是对系统调用的封装。
在系统调用的IO型接口中,open函数的类型是int型。
int open(const char *pathname, int flags, mode_t mode);
open函数返回的是一个文件描述符。可以通过文件描述符来找到对应的文件。
而FILE结构体中就有一个int型的变量来表示这个文件描述符,这就是为什么C中的IO也可以找到对应的文件,这就是一种封装。
我们来看一下FILE结构体中的代码。
在/sur/include/stdio.h 可以找到
FILE中的int _fileno就是对文件描述符的封装。
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; //封装的文件描述符
//……
我们来看下面一段代码:
#include
#include
#include
#include
#include
int main()
{
close(1);
int fd=open("myfile",O_WRONLY|O_CREAT,0666);
const char* arr="hello linux\n";
fwrite("arr",strlen(arr),1,stdout);
return 0;
}
字符串并没有打印到显示器上,而是被写入到了myfile文件中。
为什么呢?
在这之前先要了解,C中的stdin、stdout、stdree这三个流都是FILE*型的,并且这三这FILE中的文件描述符被固定为0、1、2。这就是为什么在C中用stdin、stdout、stderr就能找到键盘、显示器、显示器。
而在上面代码中关闭了1,myfile的文件描述符是1,所以在fwrite函数中用stdout还是写入到了myfile文件中。这就是为什么显示器上没有打印,而写入到了myfile文件的原因。而通过这段代码,我们现在应该要了解FILE*是什么了。
最后,fopen究竟做了什么?
1、给调用的用户申请struct FILE结构体变量,并返回地址(FILE*)
2、在底层通过open打开文件,并返回fd,把fd填充进FILE变量的fileno中。
有两段代码:
#include
#include
void A()
{
printf("hello linux\n");
sleep(3);
}
void B()
{
printf("hello linux");
sleep(3);
}
int main()
{
A();
B();
return 0;
}
其中A是先显示hello Linux,再等待3秒。
B是先等待3秒,再显示hello Linux。
把内容回显给显示器时,内容先被写入缓冲区,采用的是行缓存,当遇到\n时就会刷新缓冲区,把内容写人显示器中,当缓冲区内容被写满时也会刷新缓冲区。
缓冲区有:
(常见对显示器内容刷新,用的是行缓存,这样才能更快的看到我们的内容)
看代码:
#include
#include
#include
int main()
{
printf("hello printf\n");
fprintf(stdout,"hello fprintf\n");
const char*mag2="hello write\n";
write(1,mag2,strlen(mag2));
fork();
return 0;
}
运行结果是:
hello printf
hello fprintf
hello write
但是我们对该进程进行输入重定向到文件中:./a.out > myfile
hello write
hello printf
hello fprintf
hello printf
hello fprintf
当我们重定向后,文件描述符1已经不表示显示器了,而是我们的文件。这时候,缓冲区采用的是全缓存。
系统调用的IO接口是无缓冲,可以直接写入。
当缓冲区中存放了“hello printf\n”和“hello fprintf\n”时,创建了子进程,在return 0;之前进行了写时拷贝,所以最后打印了两次字符串。
这里面的缓冲区是C提供的,也是由FILE结构体进行维护。
缓冲区是在内存中的,在用户层。
缓冲区的数据刷新不是直接刷新到文件中,而是要经过内核区再写入到文件,这里有OS自己的刷新机制,这里不谈(我还没学到,哈哈)。
fclose:在关闭1之前,刷新了C中的缓冲区。内容可以被写入到文件内。
#include
#include
#include
#include
#include
#include
int main()
{
close(1);
int fd=open("myfile",O_WRONLY|O_CREAT,0666);
char *arr="hello linux\n";
fprintf(stdout,arr);
fclose(stdout);
return 0;
}
close:由于采用了全缓,当close(1)时,系统调用的看不见C的缓冲区,没有刷新缓冲区就关掉了,故没有写入到文件中。
#include
#include
#include
#include
#include
#include
int main()
{
close(1);
int fd=open("myfile",O_WRONLY|O_CREAT,0666);
char *arr="hello linux\n";
fprintf(stdout,arr);
//fflush(stdout);//可以在调用close之前,先刷新缓冲区。
fclose(stdout);
return 0;
}
在上面的重定向中,我们要先close(1),再打开文件,这样好繁琐。我们有一个更简单的方法。
int dup2(int oldfd, int newfd);
#include
#include
#include
#include
#include
#include
int main()
{
int fd=open("myfile",O_WRONLY|O_CREAT,0666);
char *arr="hello linux\n";
close(1);
dup2(fd,1);
printf("%s",arr);
return 0;
}
文件系统是Linux的一个重要部分,在Linux中玩了有一段时间,我一直有一个困惑,文件是怎么创建出来的?通过学习,慢慢的我自己有了一点了解。
文件=文件的属性+文件的内容,我们在查看文件大小时,显示的是文件内容的大小,其属性信息并没有算在里面,这说明了,文件的属性和文件的内容是分离存储的,在磁盘上。文件属性叫做元信息。
我们先简单了解磁盘:
磁盘有扇区、磁道、柱面、磁头……
文件的写入到磁盘中,会对磁盘寻址,其中会对柱头、磁道、扇面来寻找要写入的内容的地方。
假设磁盘的大小为500GB,对这么大的空间进行管理,系统采用了分区(就像中国也有省,市,县一样)
inode是任何一个文件属性的集合,Linux中几乎每一个文件都有一个inode编号。
文件的元信息就是保存在inode中的,inode是一个结构体。
上图为磁盘文件的系统图(内核内存映射肯定有所不同),磁盘是一个典型的块设备,磁盘的分区被划分为一个个block。一个block的大小是由格式化的时候确定的,并且不可以更改。例如mke2fs的-b选项可以设定block大小为1024、2048或4096字节。而上图中启动块(Boot Block)的大小是确定的。
数据区中是一个一个的块,每个块的大小是4KB,用来存放数据。(存在多级索引,我还没学,就不讲了)
inode结构体中有一个数组(int block[12])记录块(Data blockse)的位置。
一个普通文件的创建。
先要去inode位图中找到未被使用的inode,并申请下来把文件的属性记录其中,如果要对该文件写入内容,则系统会根据内容的大小去块位图中申请所需要的空闲块,并写入内容。内核在inode上的磁盘分布区记录了上述块列表。之后,内核会把该文件的inode编号和文件名添加到所在目录文件中。该文件的inode编号和该文件的文件名对应起来。
目录的创建
目录也是文件,也有自己的inode编号。目录在创建的过程中和上面普通文件的创建有点类似,不同的是,目录文件的内容是存放目录下的文件名和inode指针,使这些文件名和inode指针一一对应起来。
ls 命令:
ls -l 命令
这也可以看出,目录和文件之前的联系。
文件的删除
文件的删除并没有那么复杂,只要把对应inode的位图中的数据改掉(把1置成0),对应块位图数据也修改掉(把1置成0)。这也就是为什么删除的文件可以恢复过来的原因,只要把位图再置回来。
创建一个新文件主要有一下4个操作:
ln 文件名 要创建的文件名
这两个文件的inode号相同,说明myfile-s不是一个独立的文件,只是在目录的数据中添加了一个新文件名,该文件名对应的ionde和myfile相同。
硬连接数
硬连接数的数量是,有多少个文件对应的inode编号相同。
myfile文件和myfile-s文件的inode编号相同,所以硬连接数位2。
想要释放这个文件对应磁盘空间释放,要把硬连接数变成0。删除一个相同inode编号的文件,该硬连接数-1。
也就是说:
abc和def的链接状态完全相同,他们被称为指向文件的硬链接。内核记录了这个连接数,inode
263466 的硬连接数为2。
我们在删除文件时干了两件事情:1.在目录中将对应的记录删除,2.将硬连接数-1,如果为0,则将对应的磁盘释放。
ln -s 文件名 要创建的文件名
硬链接是通过inode引用另外一个文件,软链接是通过名字引用另外一个文件。
用stat 文件名可以查看