接口介绍
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:打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行"或"运算( '|' )
参数:
- O_RDONLY:只读打开
- O_WRONLY:只写打开
- O_RDWR:读,写打开。这三个参数必定指定一个且只能指定一个
- O_CREAT:若文件不存在,则创建它,需要使用mode选项,来指名新文件的访问权限
- O_APPEND:追加写
返回值:成功就返回新打开的文件描述符,失败就返回-1
使用:open函数具体使用哪个分场景而定,如果目标文件不存在,需要open创建,就要使用三个参数,并且设置新创建文件的权限。
int open(pathname,O_CREAT,0666);//文件不存在,创建它并设置权限为0666
OPEN函数的返回值
在学习返回值之前,先了解一下系统调用和库函数。
- 像fopen,fclose,fread,fwrite这些都是C标准库中的函数 ,称之为库函数
- 而open,close,read,write,lseek,这些都是OS提供的系统调用
所以可以认为f#系列的函数,都是队系统调用函数进行了封装,方便进行二次开发
文件描述符fd
通过上面的open函数就可以知道,文件描述符fd就是一个小整数
0&1&2:
在Linux下,默认会有三个缺省打开的文件描述符,分别是标准输入0,标准输出1,标准错误2
0对应的是键盘,1和2对应的是显示器,即0是从键盘读入,1和2是像显示器输出。
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
int main()
{char buf[1024];ssize_t s = read(0, buf, sizeof(buf));if(s > 0){buf[s] = 0;write(1, buf, strlen(buf));write(2, buf, strlen(buf));}return 0;
}
上面代码就是利用系统调用接口write像显示器输出buf中的内容,用read从键盘上读取内容保存到数组buf中.
总结:文件描述符就是从0开始的小整数。当我们打开文件时,操作系统在内存中创建相应的数据结构来描述文件,于是就有了file结构体。表示一个已经打开的文件对象。而进程执行open系统调用,所以必须让进程和文件关联起来。每个进程都有一个指针*files,指向一张表files_struct,该表最重要的部分就是包含一个指针数组,每个元素都是一个指向打开文件的指针!所以,本质上,文件描述符就是该指针数组的下标。所以只要知道文件描述符,就可以找到对应的文件.
文件描述符的分配规则
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{int fd = open("myfile", O_RDONLY);if(fd < 0){perror("open");return 1;}printf("fd: %d\n", fd);close(fd);return 0;
}
最后输出的答案是" fd: 3"
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{close(0);//close(2);int fd = open("myfile", O_RDONLY);if(fd < 0){perror("open");return 1;}printf("fd: %d\n", fd);close(fd);return 0;
}
输出的是"fd: 0"。
可以得出结论:文件描述符的分配规则:在file_struct数组中,找到当前没有被使用的最小的下标,作为新的文件描述符
重定向
#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, 0644);if(fd < 0){perror("open");return 1;}printf("fd: %d\n", fd);fflush(stdout);close(fd);exit(0);
}
这时候我们发现本来应该输出到显示器上的内容,输出到了文件myfile中,其中fd = 1.这种输出现象叫做重定向。常见的重定向有>,>>,<
一张图了解重定向本质:
dup2系统调用
函数原型:
#include <unistd.h>
int dup2(int oldfd, int newfd);
dup系统调用分配的文件描述符是由系统分配的,遵循文件描述符的分配原则,并不能指定一个文件描述符,这是dup的一个缺陷,而dup2就很好的解决了这个问题
oldfd:需要被复制的文件描述符
newfd:指定一个文件描述符(需要指定一个当前进程没有使用到的文件描述符)
返回值:成功时返回一个新的文件描述符,也就是newfd;失败就返回-1
示例:
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
int main() {int fd = open("./log", O_CREAT | O_RDWR);if (fd < 0) {perror("open");return 1;}close(1);dup2(fd, 1);for (;;) {char buf[1024] = {0};ssize_t read_size = read(0, buf, sizeof(buf) - 1);if (read_size < 0) {perror("read");break;}printf("%s", buf);fflush(stdout);}return 0;
}
跟上面的代码一个效果。刚开始创建文件的时候,根据文件描述符分配原则,获得的fd应该为3,
这个时候调用dup2(fd,1)将fd的文件描述符改成1,并断开原先的标准输出,如上述重定向图一样。
FILE
因为IO相关函数与系统调用接口对应,并且库函数封装系统调用,所以本质上,访问文件都是通过fd访问的。所以C库当中的FILE结构体内部,必定封装了fd。
借由下面一段代码来理解:
#include <stdio.h>
#include <string.h>
int main()
{const char *msg0="hello printf\n";const char *msg1="hello fwrite\n";const char *msg2="hello write\n";printf("%s", msg0);fwrite(msg1, strlen(msg0), 1, stdout);write(1, msg2, strlen(msg2));fork();return 0;
}
运行结果:
hello printf
hello fwrite
hello write
但是如果对文件进行输出重定向的话。./test > file 则文件file中会有
hello write
hello printf
hello fwrite
hello printf
hello fwrite
可以发现printf和fwrite都输出了两次,这两个都是库函数,而write只输出了一次,write为系统调用
- 一般C库函数写入文件时是全缓冲的,而写入显示器是行缓冲的
- printf fwrite库函数会自带缓冲区,当发生重定向到普通文件时候,数据的缓冲方式由行缓冲变成了全缓冲
- 而我们放在缓冲区中的数据,不会被立即刷新,甚至fork之后
- 但是进程退出之后,会统一刷新,写入文件当中
- 但是fork时候,父子数据会发生写时拷贝,所以当父进程准备刷新的时候,子进程就有了同样的一份数据,随机就会产生两份同样的数据
- write没有变化说明,write没有所谓的缓冲
综上:printf fwite库函数会自带缓冲区,而write系统调用没有带缓冲区,另外,我们这里的缓冲区都是用户级的缓冲区,作用就是提升整机性能,OS也会提供相关内核级缓冲区。
那这个用户级缓冲区由谁提供?
显然是C标准库提供,上述中printf和fwrite都是库函数,write是系统调用,库函数在系统调用的上层,是对系统调用的封装,但是write没有缓冲区,而printf和fwrite有缓冲区,说明缓冲区是在封装的时候加上的,也就是由C标准库提供的。
理解文件系统
在linux下使用指令ls -l
[root@localhost linux]# ls -l
-rwxr-xr-x. 1 root root 7438 "1月 1 14:56" a.out
-rw-r--r--. 1 root root 654 "1月 1 14:56" test.c
每行包含7列:
- 模式
- 硬连接数
- 文件所有者
- 组
- 大小
- 最后修改时间
- 文件名
inode:
为了解释清楚inode,先简单了解一下文件系统
磁盘是典型的块设备,硬盘分区被划分为一个个的block。一个block的大小是由格式化的时候确定的,不可以更改。下图中的启动快(Boot Block)的大小是可以确定的
- Block Group:ext2文件系统会根据分区的大小划分为数个Block Group。每个BlockGroup都有相同的结构组成。
- 超级块(Super Block):存放文件系统本身的结构信息,记录的信息主要有:block和inode的总量,未使用的block和inode的数量,一个block和inode的大小,最近一次挂载的时间,最近一次写入数据的时间,最近一次检验磁盘的时间等其他文件系统的相关信息。Super Block中哪个数据块已经被占用,哪个数据块没有被占用
- 块位图(Block BItmap):Block Bitmap中记录者Data Block中哪个数据块已经被占用,哪个数据块没有被占用
- inode位图(inode Bitmap):每个bit表示一个inode是否可空闲使用
- i节点表:存放文件属性 如,文件大小,所有者,最近修改时间
- 数据区:存放文件内容
将属性和数据分开存放的想法看起来很简单,看下图。
[root@localhost linux]# touch abc
[root@localhost linux]# ls -i abc
261234 abc
创建一个新文件主要有以下四个操作
- 存储属性内核先找到一个空闲的i节点(这里是261234)。内核把文件信息记录到其中
- 存储数据该文件需要存储在三个磁盘块,内核找到了三个空闲块:300,500,800。将内核缓冲区的第一块数据复制到300,下一块复制到500,以此类推.
- 记录分配情况文件内容按顺序300,500,800存放。内核在inode上的磁盘分布区记录了上述块列表。
- 添加文件名到目录
新的文件名字abc。内核将入口(261234,abc)添加到目录文件。文件名和inode之间的对应关系将文件名和文件的内容及属性连接起来(建立映射关系)。
软硬链接
硬链接 :
特点:
- 具有相同inode节点号的多个文件互为硬链接 文件
- 删除硬链接 文件或者删除源文件任意之一,文件实体并未被删除
- 只有删除了源文件和所有对应的硬连接文件,文件实体才会被删除
- 硬链接 文件是文件的另一个入口
- 可以用过给文件设置硬连接文件来防止重要文件被误删
- 可以通过ls -i看到index;
- 硬链接文件是普通文件,可以用rm删除
- 对于静态文件(没有进程正在调用),当硬链接数为0时文件就被删除。注意:如果有进程正在调用,则无法删除或者及时文件名被删除空间也不会释放
我们看到,真正找到磁盘上文件的并不是文件名,而是inode。 其实在linux中可以让多个文件名对应于同一个inode
[root@localhost linux]# touch abc[root@localhost linux]# ln abc def
[root@localhost linux]# ls -1i
abc def 261234 abc 261234 def
- abc和def的链接状态完全相同,他们被称为指向文件的硬链接。内核记录了这个链接数,inode261234 的硬连接数为2
- 我们在删除文件时干了两件事情:1.在目录中将对应的记录删除,2.将硬链接数-1,如果为0,则将对应的磁盘释放
软链接 :
特点:
- 软链接 类似windows系统的快捷方式
- 软链接里面存放点的是源文件的路径
- 删除源文件,软链接依然存在,但无法访问源文件内容
- 软链接失效时一般是白字红底闪烁
- 创建软链接命令ln -s 源文件 软链接文件;
- 软链接文件和源文件是不同的文件,稳健类型也不同,inode号也不同
- 软链接的文件类型是"I",可以用rm删除
硬链接是通过inode引用另外一个文件,软链接是通过名字引用另外一个文件,在shell中的做法
263563 -rw-r--r--. 2 root root 0 9月 15 17:45 abc
261678 lrwxrwxrwx. 1 root root 3 9月 15 17:53 abc.s -> abc
263563 -rw-r--r--. 2 root root 0 9月 15 17:45 def