操作文件的系统接口
open/close 函数
open 函数, 打开一个文件.
返回值: 返回一个文件标识符 fd, int 类型数据
pathname: 想要打开的文件的路径
flags: 打开文件的方式. (这是一个位图, 比特位为 1, 代表使用某个方式, 为 0 则不启用)
打开文件的方式:
O_RDONLY: 以只读的方式打开文件
O_WRONLY: 以只写的方式打开文件
O_RDWR: 以读写的方式打开文件
O_CREAT: 如果想要打开的文件不存在, 则创建这个文件. mode 是新创建文件的权限
O_APPEND: 以追加内容的方式打开文件. (上面三种打开文件的方式, 都是直接将原数据覆盖)
int fd = open("example.txt", O_WRONLY | O_CREAT);
// O_WRONLY | O_CREAT, 或运算得到的结果中, 比特位为 1 的就是打开的方式
close函数则非常简单, 关闭文件没有什么方式, 只需要将要关闭的文件的文件描述符作为参数传递就行.
返回值: 关闭成功返回0, 失败时返回 -1
fd: 文件描述符, 就是使用 open 函数得到的返回值
int fd = open("example.txt", O_WRONLY);
close(fd);
read/write 函数
read函数, 读取文件中的内容
返回值: 返回本次实际读取的字节数
fd: 要读取的文件描述符
buf: 读取出来的数据存储在 buf 中
count: 本次想要要读取多少数据 (单位字节)
char buffer[1024];
int fd = open("text.txt",O_WRONLY);
ssize_t n = read(fd,buffer,sizeof(buffer));
if(n > 0)
{
buffer[n] = '\0';//将字符串变成C语言风格,以\0结尾
}
write函数, 从某个文件中读取数据
返回值: 成功向文件中写入了多少数据 (单位字节)
fd: 想要写入的文件的文件描述符
buf: 将 buf 中的内容写入到文件中
count: 写入多少数据 (单位字节)
char* buffer = "abcdef";
int fd = open("text.txt",O_WRONLY);
write(fd,buffer,sizeof(buffer));
文件描述符
从上面的接口可以看到, 我们一直在提一个东西: 文件描述符
我们也知道了, 文件描述符就是一个整数.
那么为什么文件描述符是一个整数, 这个整数有什么意义? 我们多创建几个文件描述符观察一下
int main()
{
int fd1 = open("text1.txt",O_WRONLY | O_CREAT);
int fd2 = open("text2.txt",O_WRONLY | O_CREAT);
int fd3 = open("text3.txt",O_WRONLY | O_CREAT);
int fd4 = open("text4.txt",O_WRONLY | O_CREAT);
printf("%d, %d, %d, %d",fd1,fd2,fd3,fd4);
return 0;
}
当我们执行上面的代码之后, 能观察到打印出来的四个文件描述符分别是 3, 4, 5, 6.
那么为什么是 3, 4, 5, 6. 数字 0, 1, 2 去哪了?
首先需要了解, 在C/C++程序中,
在运行时会默认打开 stdin(标准输入), stdout(标准输出), stderr(标准错误) 三个文件.
那么 0, 1, 2 这三个数也就是被这三个文件使用了. (这三个文件是什么后面说)
我们可以看到, 文件描述符的创建是递增的, 那如果前面有文件关闭了,
下一次打开文件时, 文件描述符又会是哪一个呢?
int main()
{
int fd1 = open("text1.txt",O_WRONLY | O_CREAT);
int fd2 = open("text2.txt",O_WRONLY | O_CREAT);
int fd3 = open("text3.txt",O_WRONLY | O_CREAT);
int fd4 = open("text4.txt",O_WRONLY | O_CREAT);
printf("%d, %d, %d, %d\n",fd1,fd2,fd3,fd4);
close(fd3);
int fd5 = open("text5.txt",O_WRONLY | O_CREAT);
printf("%d\n", fd5);
return 0;
}
当前面的文件关闭之后,再创建新的文件描述符,
那么新的文件描述符就是被关闭的那个, 而不是从最大的文件描述符 + 1.
上面观察到了文件描述符的规律, 那么文件描述符底层是什么?
OS (操作系统) 使用一个结构体来管理文件, 就像管理进程一样.
struct file
{
// 文件内容
// 文件属性
// ....
file* next;
file* prev;
}
OS (操作系统) 中打开的文件肯定不止一个,
为了方便管理这些文件, 操作系统会将这些文件的结构体像链表一样, 串联起来.
这样只需要遍历这个链表, 就能管理所有的已打开的文件.
上面的都是 OS (操作系统) 对文件的管理, 那么进程是怎么管理自己的文件的呢?
对于每一个进程, 都会有一张文件描述符表 (本质就是一个数字).
使用 open 函数得到的 fd, 实际上就是文件描述符表的下标.
当需要访问某个文件时, 就通过下标找到文件地址, 然后访问.
当需要创建一个文件描述符表时, 就会遍历数组, 找到了一个位置没有被使用,
就会使用这个位置. 所以当有文件被关闭时, 又打开一个文件, 得到的描述符时一样的.
三个默认打开的文件: stdin(标准输入), stdout(标准输出), stderr(标准错误).
stdin: 其实就是从键盘中读取数据 (scanf)
stdout: 向屏幕输出数据 (printf 函数)
stderr: 其实和stdout是一样的, 像屏幕输出数据
重定向
上面我们知道, stdout 是向屏幕输出数据的
那么如果我将 stdout 先关掉,然后再打开一个文件.
此时新打开的文件返回的文件描述符就是 1.
int main()
{
close(1);
int fd = open("log.txt",O_WRONLY | O_CREAT | O_TRUNC, 0666);
printf("fd: %d\n",fd);
fprintf(stdout,"hello fprintf,我是一号文件描述符\n");
// 向 stdout 中写入数据 "hello fprintf,我是一号文件描述符\n"
return 0;
}
在代码中, 我们先关闭 1 文件, 然后新打开一个文件, 那么这个新打开的文件描述符就是 1.
那么此时再向 stdout 中输入数据, 就会输入到 log.txt 中.
stdout 只认文件描述符表中下标为 1 的文件, 不管 1 下标中的文件是什么.
这就是一种重定向: 通过修改 fd 所指向的内容, 就能达到改变输入输出的文件
dup2 函数
如果每次重定向都要先关闭一个文件,
然后再打开一个文件, 还是过于麻烦.
我们可以直接使用系统提供的 dup 和 dup2 函数
oldfd: 要复制的文件描述符
newfd: 新的文件描述符的数值
返回值: 成功返回新的文件描述符 newfd, 失败返回 -1.
用的基本都是 dup2 函数
open("log.txt",O_WRONLY | O_CREAT);
dup2(3,1);
最开始 1 指向屏幕, 现在我们将 3下标中的内容复制到 1 中, 所以最后 1 指向的就是 log.txt 文件了.
软硬链接
在 Linux 中, 并不依靠文件的名称来标识文件,
在 Linux 中会为每一个文件分配一个唯一的标识 inode.
使用 ls -il 指令
第一行 打印出来的就是文件的 inode, 后面的那个 1 可以简单的理解为, 这个文件有几个名称 (引用计数).
因为 Linux 中不依靠文件名来区分, 所以一个文件有可能存在一个 inode 对应 多个文件名.
硬链接
使用指令: ln 已存在文件名称 新文件名称
可以看到原 tmp.cc 文件的引用计数变为了2, 并且 tmp.cc 和 tmp-copy.cc 的 inode 都一样,
这就代表这两个文件实际上是同一个, 只是名称不同.所以 硬链接 本质就是对文件增加了一个新的别名.
软链接
使用指令: ln -s 原文件名 新文件名
可以观察到, 新创建出来的 tmp-copy 文件, 拥有一个独立的 inode, tmp-copy 是一个独立文件, 并不是 tmp.cc 的别名. 这就相当于是 Windows 下的快捷方式.
通过这个文件, 可以很便捷的找到要使用的文件, Windows 中的程序, 我们可能都不记得在那个文件, 但是我们只需要双击微信的快捷方式, 就能直接运行微信. 不用在一微信在哪个文件下.