接口介绍
#include
#include
#include
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
pathname:要打开或创建的目标文件
flags:打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行"或"运算( '|' )
参数:
返回值:成功就返回新打开的文件描述符,失败就返回-1
使用:open函数具体使用哪个分场景而定,如果目标文件不存在,需要open创建,就要使用三个参数,并且设置新创建文件的权限。
int open(pathname,O_CREAT,0666);//文件不存在,创建它并设置权限为0666
OPEN函数的返回值
在学习返回值之前,先了解一下系统调用和库函数。
所以可以认为f#系列的函数,都是队系统调用函数进行了封装,方便进行二次开发
文件描述符fd
通过上面的open函数就可以知道,文件描述符fd就是一个小整数
在Linux下,默认会有三个缺省打开的文件描述符,分别是标准输入0,标准输出1,标准错误2
0对应的是键盘,1和2对应的是显示器,即0是从键盘读入,1和2是像显示器输出。
#include
#include
#include
#include
#include
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
#include
#include
#include
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
#include
#include
#include
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
#include
#include
#include
#include
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
int dup2(int oldfd, int newfd);
dup系统调用分配的文件描述符是由系统分配的,遵循文件描述符的分配原则,并不能指定一个文件描述符,这是dup的一个缺陷,而dup2就很好的解决了这个问题
oldfd:需要被复制的文件描述符
newfd:指定一个文件描述符(需要指定一个当前进程没有使用到的文件描述符)
返回值:成功时返回一个新的文件描述符,也就是newfd;失败就返回-1
示例:
#include
#include
#include
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
#include
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,先简单了解一下文件系统
磁盘是典型的块设备,硬盘分区被划分为一个个的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。 其实在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,则将对应的磁盘释放
特点:
硬链接是通过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