在讲 lseek 前,先讲讲上一节文件描述符这个遗留的问题。
1. 文件描述符
在上一节中,我们已经知道 open 函数返回一个整数,它在本进程中唯一的标识了一个文件。那么,它到底是怎样标识的呢?
可以设想,在一个进程中,存在着一个大数组(记录了打开的文件),这个数组的索引号就是 open 函数返回的整数,而这个数组的每一项,记录了打开的文件相关信息。
我们知道,在操作系统中,是通过进程控制块(PCB)来描述进程信息和相关资源的。实际上在 linux 中,它就是一个巨大的结构体,在 linux 0.11 中,就是 task_struct 结构体。为了便于学习,我把其它用不着的部分先删除了。(代码来源于 linux 0.11,因为我们是初学者,学习这个版本的源码足够了。)
从下面的PCB结构体中,确实存在着这么一个数组 filp[NR_OPEN]
。
struct task_struct {
long pid; // 进程号
// ... 其它字段
// 文件描述符标志位,有关该标志位,以后再说。
unsigned long close_on_exec;
// 数组索引号就是文件描述符。NR_OPEN 的值在 linux 0.11 中被定义为 20
struct file * filp[NR_OPEN];
// ... 其它字段
};
那么 struct file
长啥样呢?我们知道它记录了文件的相关信息,还记得open
函数的几个参数吗?有 flag
, 它记录了文件状态,比如O_RDONLY,O_WRONLY,O_APPEND
等等?有 mode
,文件的权限位。可以猜测,这个struct file 也记录着这些属性。下面是 linux 0.11 源码中的数据结构。
struct file {
unsigned short f_mode; // 文件权限位
unsigned short f_flags; // 文件状态位
unsigned short f_count; // 引用计数
struct m_inode * f_inode; // 文件存在磁盘上的哪个位置等等其它信息由这个字段来解释
off_t f_pos; // 当前偏移量
};
用下面这张图来描述 PCB,文件描述符,文件表之间的关系,会更加清晰。
相信到这里,你已经理清了描述符,文件表之间的关系了。在上图中,可以看到本进程中有三个文件描述符指向了同一个文件表,那么这个文件表中的引用计数必然为 3。用 C 语言来讲就是 flip[0] == flip[1] && flip[1] == flip[2]
。而第二个文件表只有一个指针指向了它,它的引用计数就是1.在调用 close(fd) 函数的时候,实际上做了两步:
--flip[fd]->count;
flip[fd] = NULL;
当 count == 0 的时候,才真正的关闭文件。
有没有可能人为的让两个不同的描述符指向同一个文件表?答案是完全可能,这没什么好奇怪的,函数 dup 和 dup2 就是干这事的。dup2(int oldfd, int newfd) 的函数做的事情大概就向下面这个样子:
int dup2(int oldfd, int newfd) {
// ...
close(newfd);
flip[newfd] = flip[oldfd];
flip[newfd]->count++;
//...
return 0;
}
而 dup 函数只接受一个参数 oldfd,它返回系统分配的新描述符值。
并非所有的操作系统实现方式都如图 1 中所示,在 linux 0.11 中采用的是数组,当然,完全可以采用链表来实现,这取决于不同操作系统的实现方式,万变不离其宗。
2. lseek 函数
- 函数原型
// off_t 可以理解成 int
off_t lseek(int fd, off_t offset, int whence);
lseek 函数,就是改变 flip[fd] 指向的 struct file 这个结构中的 f_pos 成员的。
当用 open 函数打开一个文件的时候,该偏移量 f_pos
被默认指定为 0。
- 如果 whence 等于
SEEK_SET
,则f_pos = offset
(offset 只能是正数) - 如果 whence 等于
SEEK_CUR
, 则f_pos = f_pos + offset
(offset 可正可负) - 如果 whence 等于
SEEK_END
,则f_pos = 文件长度 + offset
(offset 可正可负)
3. 指定 f_pos 后有什么影响?
如果一个文件中的内容是 hello world
,当f_pos==6
的时候,执行 read
函数将从字母w
开始读取,执行write
也会从 w
处开始写数据。
4. 示例
- 目录树
.
|-lseekdemo.c
|-test
- test 文件
hello world
- lseekdemo.c
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
int main() {
int fd = open("test", O_RDONLY);
if (fd < 0) {
perror("open");
return 1;
}
char buf[64] = {0};
lseek(fd, 6, SEEK_SET);
read(fd, buf, 64);
printf("%s\n", buf);
return 0;
}
编译后执行,屏幕会打印 world
.
5. 总结
- 理解文件描述符是什么
- 知道 struct file 结构体
- 理解 lseek 原理
- 掌握 lseek 用法