6-文件IO-文件描述符与lseek

在讲 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,文件描述符,文件表之间的关系,会更加清晰。

这里写图片描述

图1 PCB,文件描述符与文件表
上图中的 `flip` 数组的索引号,就是文件描述符。数组中的每个元素是一个指向了 `struct file` 类型(文件表)的指针,`struct file` (文件表)中记录了当前打开的文件的重要信息,其中根据 f_inode 成员就可以找到磁盘上的文件(实际由 f_inode 到磁盘文件没有图中那样简单,要稍稍复杂点,不过为了说明问题,大家暂时可以这样理解,后面到文件系统的时候会深入)。

相信到这里,你已经理清了描述符,文件表之间的关系了。在上图中,可以看到本进程中有三个文件描述符指向了同一个文件表,那么这个文件表中的引用计数必然为 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 用法
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值