引言:
我们之前已经学过了C的IO流,这两者语言层面的IO相较于系统的IP流是较为上层的,C中的FILE*就封装有系统底层的文件操作,今天就让我们学习linux系统层面的输入输出流,从底层开始认识基础IO。
系统文件I/O
- 写入文件:
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<string.h>
int main()
{
umask(0);//取消默认缺省值
int fd = open("test", O_WRONLY|O_CREAT, 0644);
int count = 10;
const char* str = "hellow ALA\n";
while(count--)
{
write(fd, str, strlen(str));
}
close(fd);
return 0;
}
- 读出文件:
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<string.h>
int main()
{
umask(0);
int fd = open("test", O_RDONLY);
char buff[1024];
const char* str = "hellow ALA\n";
while(1)
{
ssize_t s = read(fd, buff, strlen(str));
if(s > 0)
{
printf("%s", buff);
}
else
break;
}
close(fd);
return 0;
}
-
结果:
-
open接口介绍:
返回值:成功,返回新的文件描述符,失败,返回-1参数:
- pathname:要打开的目标文件
- flags:打开文件时,传入多个选项,设置为只读/只写/读写等
- O_RDONLY:只读打开
- O_WRONLY:只写打开
- O_RDWR:读,写打开。
文件描述符fd
-
从open函数我们知道了,文件描述符可以认为是一个小整数。
-
0&1&2
- 每个文件运行结束时,默认打开三个字节流:
- stdin(标准输入,键盘)
- stdout(标准输出,显示器)
- stderr(标准错误,显示器)
让我们尝试着使用这三个字节流:
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<string.h>
int main()
{
const char* str = "hellow ALA\n";
write(1, str, strlen(str));
write(2, str, strlen(str));
return 0;
}
结果:通过write在显示器上成功将hellow ALA输出
- 文件与文件描述符的映射关系:
解释:要管理文件描述符,就必须先描述再组织。用文件描述符数组(fd_array[])将其描述起来,在通过files_struct将其组织起来。
本质:文件描述符的本质实际上就是数组下标。
文件描述符的分配:
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<string.h>
int main()
{
int fd0 = open("test0.txt", O_WRONLY|O_CREAT, 0644);
int fd1 = open("test1.txt", O_WRONLY|O_CREAT, 0644);
int fd2 = open("test2.txt", O_WRONLY|O_CREAT, 0644);
int fd3 = open("test3.txt", O_WRONLY|O_CREAT, 0644);
int fd4 = open("test4.txt", O_WRONLY|O_CREAT, 0644);
printf("fd0:%d\n", fd0);
printf("fd1:%d\n", fd1);
printf("fd2:%d\n", fd2);
printf("fd3:%d\n", fd3);
printf("fd4:%d\n", fd4);
close(fd0);
close(fd1);
close(fd2);
close(fd3);
close(fd4);
return 0;
}
结果:
可以看到,除了前三个系统默认打开的描述符之外,其他的文件的文件描述符依次递增。
重定向
原理:
“<”输入重定向,关掉stdin(0),从文件中读取数据
“>”输出重定向,只重定向一号描述符,以写入的方式,写到指定文件中
“>>”追加重定向,将显示器中的数据,以追加的形式写到文件末尾
尝试实现重定向:
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<string.h>
int main()
{
close(1);
int fd0 = open("test0.txt", O_WRONLY|O_CREAT, 0644);
int fd1 = open("test1.txt", O_WRONLY|O_CREAT, 0644);
int fd2 = open("test2.txt", O_WRONLY|O_CREAT, 0644);
int fd3 = open("test3.txt", O_WRONLY|O_CREAT, 0644);
int fd4 = open("test4.txt", O_WRONLY|O_CREAT, 0644);
printf("fd0:%d\n", fd0);
printf("fd1:%d\n", fd1);
printf("fd2:%d\n", fd2);
printf("fd3:%d\n", fd3);
printf("fd4:%d\n", fd4);
fflush(stdout);
close(fd0);
close(fd1);
close(fd2);
close(fd3);
close(fd4);
return 0;
}
结果:在显示器上没有输出,输出结果全部打到了text0.txt中。
原因:关闭标准输入后,test0.text文件占用了1号文件描述符,原本要写在显示器的输出,全部写入了test0.tex这个文件中。
示意图:
- dup2系统调用:
#include<unistd.h> int dup2(int oldfd, int newfd);
- 作用:让newfd称为oldfd的一个拷贝。
FILE:
-
原理:由于IO相关函数与系统调用接口对应,并且库函数封装系统调用,因此两者是上下级关系,因此从本质上来说,访问文件都是通过fd访问的。FILE*中必定封装了fd(文件描述符)。
-
举例:
fwrite通过“FILE*”(文件指针,对file_struct的封装),再从file_struct中将fd_array[](文件描述符数组找到),通过linux底层调用write操作,在对应的文件描述符中进行“写”操作。
让我们看看下面这段代码:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<sys/types.h>
int main()
{
const char* str0 = "hellow printf\n";
const char* str1 = "hellow fwrite\n";
const char* str2 = "hellow write\n";
printf("%s", str0);
fwrite(str1, strlen(str1), 1, stdout);
write(1, str2, strlen(str2));
fork();
return 0;
}
正常运行的结果:
但是如果将导入文件中的话:
我们发现Printf和fwrite(库函数)都输出了两次,而write只输出了一次,为什么?
- 1.因为fwrite和printf作为库函数,都带有缓冲区,当发生重定向到文件中,缓冲方式会从行缓冲转换为全缓冲。
- 2.放在缓冲区的数据不会立即刷新,只会在进程退出后刷新,写入文件中
- 3.但是fork之后,因为父子数据会发生写时拷贝,因此父进程刷新时,子进程也有了同样的数据,产生了一样的代码
- 4.然而write不会刷新,说明其没有缓冲区。
理解文件系统
当我们使用ls -l的时候,不仅看到了文件名,还看到了文件属性
ls -l的工作原理:
-
当输入ls -l之后,程序变成了进程,进程通过操作系统访问硬盘中的文件,文件分为两个部分:文件信息(属性)和文件内容。操作系统通过访问文件属性,再将文件属性打印到电脑显示器上实现功能。
-
下面让我们来看看文件属性(inode)是什么样的。
我们先通过stat查看文件属性
可以通过ls -i查看Inode号:
文件具体分为5个部分:
- 1.超级块:存放文件系统本身的结构信息。
- 2.inode位图:每个bit表示一个inode是否可以使用
- 3.inode区:用来存放inode信息
- 4.数据位图:每个bit表示一个位置的数据区能否使用
- 5.数据区:用来存放数据
但是我们在日常操作中,看不到Inode号,我们是如何使用它的呢?
由于Linux下一切皆文件,所以目录也是文件,目录存放inode和文件名的映射关系,并通过目录维护inode和文件名之间的映射关系。
因此我们可以更系统的解释ls -l是如何实现的
linux一切皆文件 -> 目录是文件->目录中维护着inode和文件名的映射关系->操作系统依靠打开目录,提出映射关系,经过inode把属性打印在显示器上。
创建文件:
- 1.找inode位图中为0的位置
- 2.由0->1,往inode区对应的位置放入属性,分配inode号
- 3.将块位图中为0的位置
- 4.由0->1,然后在块区中对应的位置放入数据。
查文件:
- 1.查到inode号
- 2.在inode位图中找到对应的Inode编号
- 3.找到对应的数据,从当前数据开始读数据大小长度的数据块到磁盘中
删除文件:
- 1.Inode位图由1->0
- 2.对应块位图由1->0
- 3.将目录中文件名与Inode的映射关系去除