一、fd是什么
1.为什么读写文件要先open()?
看下面代码,通常我们读写文件,都是要先open,然后读写,处理完业务之后还有close一下。
fd = open(pathname, flags, mode); rlen = read(fd, buf, count); wlen = write(fd, buf, count); close(fd); |
为什么读写文件都要先open()一下,读写完又要close()呢?
以前老师只告诉我们要open和一定一定要close,有时候我们忘记close代码也不报错,一样正常运行。
为什么?希望我们带着好奇心,往下寻找答案
2.要理解文件描述符fd,要从启动进程开始
先看图,网友画的特别好,借用一下
(1)启动进程,系统会创建进程控制块来存储进程信息,task_struct,产生一个pid,叫进程描述符。
/*
进程创建时,创建的结构体,进程创建的时候返回进程描写符:pid
*/
struct task_struct {
//打开文件信息
struct files_struct *files;
volatile long state; //说明了该进程是否可以执行,还是可中断等信息
unsigned long flags; //Flage 是进程号,在调用fork()时给出
int sigpending; //进程上是否有待处理的信号
mm_segment_t addr_limit; //进程地址空间,区分内核进程与普通进程在内存存放的位置不同
//0-0xBFFFFFFF for user-thead
//0-0xFFFFFFFF for kernel-thread
//调度标志,表示该进程是否需要重新调度,若非0,则当从内核态返回到用户态,会发生调度
volatile long need_resched;
int lock_depth; //锁深度
long nice; //进程的基本时间片
//进程的调度策略,有三种,实时进程:SCHED_FIFO,SCHED_RR, 分时进程:SCHED_OTHER
unsigned long policy;
struct mm_struct *mm; //进程内存管理信息
int processor;
//若进程不在任何CPU上运行, cpus_runnable 的值是0,否则是1 这个值在运行队列被锁时更新
unsigned long cpus_runnable, cpus_allowed;
struct list_head run_list; //指向运行队列的指针
unsigned long sleep_time; //进程的睡眠时间
//用于将系统中所有的进程连成一个双向循环链表, 其根是init_task
struct task_struct *next_task, *prev_task;
struct mm_struct *active_mm;
struct list_head local_pages; //指向本地页面
unsigned int allocation_order, nr_local_pages;
struct linux_binfmt *binfmt; //进程所运行的可执行文件的格式
int exit_code, exit_signal;
int pdeath_signal; //父进程终止时向子进程发送的信号
unsigned long personality;
//Linux可以运行由其他UNIX操作系统生成的符合iBCS2标准的程序
int did_exec:1;
pid_t pid; //进程标识符,用来代表一个进程
pid_t pgrp; //进程组标识,表示进程所属的进程组
pid_t tty_old_pgrp; //进程控制终端所在的组标识
pid_t session; //进程的会话标识
pid_t tgid;
int leader; //表示进程是否为会话主管
struct task_struct *p_opptr,*p_pptr,*p_cptr,*p_ysptr,*p_osptr;
struct list_head thread_group; //线程链表
struct task_struct *pidhash_next; //用于将进程链入HASH表
struct task_struct **pidhash_pprev;
wait_queue_head_t wait_chldexit; //供wait4()使用
struct completion *vfork_done; //供vfork() 使用
unsigned long rt_priority; //实时优先级,用它计算实时进程调度时的weight值
//it_real_value,it_real_incr用于REAL定时器,单位为jiffies, 系统根据it_real_value
//设置定时器的第一个终止时间. 在定时器到期时,向进程发送SIGALRM信号,同时根据
//it_real_incr重置终止时间,it_prof_value,it_prof_incr用于Profile定时器,单位为jiffies。
//当进程运行时,不管在何种状态下,每个tick都使it_prof_value值减一,当减到0时,向进程发送
//信号SIGPROF,并根据it_prof_incr重置时间.
//it_virt_value,it_virt_value用于Virtual定时器,单位为jiffies。当进程运行时,不管在何种
//状态下,每个tick都使it_virt_value值减一当减到0时,向进程发送信号SIGVTALRM,根据
//it_virt_incr重置初值。
unsigned long it_real_value, it_prof_value, it_virt_value;
unsigned long it_real_incr, it_prof_incr, it_virt_value;
struct timer_list real_timer; //指向实时定时器的指针
struct tms times; //记录进程消耗的时间
unsigned long start_time; //进程创建的时间
//记录进程在每个CPU上所消耗的用户态时间和核心态时间
long per_cpu_utime[NR_CPUS], per_cpu_stime[NR_CPUS];
//内存缺页和交换信息:
//min_flt, maj_flt累计进程的次缺页数(Copy on Write页和匿名页)和主缺页数(从映射文件或交换
//设备读入的页面数); nswap记录进程累计换出的页面数,即写到交换设备上的页面数。
//cmin_flt, cmaj_flt, cnswap记录本进程为祖先的所有子孙进程的累计次缺页数,主缺页数和换出页面数。
//在父进程回收终止的子进程时,父进程会将子进程的这些信息累计到自己结构的这些域中
unsigned long min_flt, maj_flt, nswap, cmin_flt, cmaj_flt, cnswap;
int swappable:1; //表示进程的虚拟地址空间是否允许换出
//进程认证信息
//uid,gid为运行该进程的用户的用户标识符和组标识符,通常是进程创建者的uid,gid
//euid,egid为有效uid,gid
//fsuid,fsgid为文件系统uid,gid,这两个ID号通常与有效uid,gid相等,在检查对于文件
//系统的访问权限时使用他们。
//suid,sgid为备份uid,gid
uid_t uid,euid,suid,fsuid;
gid_t gid,egid,sgid,fsgid;
int ngroups; //记录进程在多少个用户组中
gid_t groups[NGROUPS]; //记录进程所在的组
//进程的权能,分别是有效位集合,继承位集合,允许位集合
kernel_cap_t cap_effective, cap_inheritable, cap_permitted;
int keep_capabilities:1;
struct user_struct *user;
struct rlimit rlim[RLIM_NLIMITS]; //与进程相关的资源限制信息
unsigned short used_math; //是否使用FPU
char comm[16]; //进程正在运行的可执行文件名
//文件系统信息
int link_count, total_link_count;
//NULL if no tty 进程所在的控制终端,如果不需要控制终端,则该指针为空
struct tty_struct *tty;
unsigned int locks;
//进程间通信信息
struct sem_undo *semundo; //进程在信号灯上的所有undo操作
struct sem_queue *semsleeping; //当进程因为信号灯操作而挂起时,他在该队列中记录等待的操作
//进程的CPU状态,切换时,要保存到停止进程的task_struct中
struct thread_struct thread;
//文件系统信息
struct fs_struct *fs;
//信号处理函数
spinlock_t sigmask_lock;
struct signal_struct *sig; //信号处理函数
sigset_t blocked; //进程当前要阻塞的信号,每个信号对应一位
struct sigpending pending; //进程上是否有待处理的信号
unsigned long sas_ss_sp;
size_t sas_ss_size;
int (*notifier)(void *priv);
void *notifier_data;
sigset_t *notifier_mask;
u32 parent_exec_id;
u32 self_exec_id;
spinlock_t alloc_lock;
void *journal_info;
}
/**
文件描述符表,
内核打开文件会从fd_array找到一个空闲的fd:文件描述符,作为open()返回值,
如果超出长度:open too many files
查看最大值:ulimit -n ,取决内存来设置
*/
struct files_struct {
/* 用于查找下一个空闲的fd */
int next_fd;
/* fd_array为一个固定大小的file结构数组。struct file是内核用于文
件管理的结构。这里使用默认大小的数组, 就是为了可以涵盖
态分配 */
struct file __rcu * fd_array[NR_OPEN_DEFAULT];
/* count为文件表files_struct的引用计数 */
atomic_t count;
/* 文件描述符表 */
/*
为什么有两个fdtable呢?这是内核的一种优化策略。fdt为指针, 而fdtab为普通变量。一般情况下,
fdt是指向fdtab的, 当需要它的时候, 才会真正动态申请内存。因为默认大小的文件表足以应付大多数
情况, 因此这样就可以避免频繁的内存申请。
这也是内核的常用技巧之一。在创建时, 使用普通的变量或者数组, 然后让指针指向它, 作为默认情况使
用。只有当进程使用量超过默认值时, 才会动态申请内存。
*//*
struct fdtable __rcu *fdt;
struct fdtable fdtab;
* written part on a separate cache line in SMP
*/
/* 使用____cacheline_aligned_in_smp可以保证file_lock是以cache
line 对齐的, 避免了false sharing */
spinlock_t file_lock ____cacheline_aligned_in_smp;
/* 保存执行exec需要关闭的文件描述符的位图 */
struct embedded_fd_set close_on_exec_init;
/* 保存打开的文件描述符的位图 */
struct embedded_fd_set open_fds_init;
/* fd_array为一个固定大小的file结构数组。struct file是内核用于文
件管理的结构。这里使用默认大小的数组, 就是为了可以涵盖
}
(2)task_struct有个成员files_struct指针指向文件描述符表结构体,files_struct里面一个定长file结构数组:fd_array,映射fd和file的关系。
(3)内核open文件时,会在这个进程里面的fd_array(文件描述符列表)中找到一个空闲的fd,然后空闲的fd和文件inode号建立映射关系(文件部分属性读入内存),并返回fd_array数组下标,这个下标值就是文件描述符,简称fd。
所以fd本质就是一个数组下标。也就是网上说的非负整数。
3.查看进程所以打开的fd
(1)#lsof -p pid
$$表示当前窗口进程的pid
lsof输出各列信息:
COMMAND:进程的名称 PID:进程标识符
USER:进程所有者
FD:文件描述符,应用程序通过文件描述符识别该文件。如cwd、txt等 .任何文件都有012
0u标准输入
1u标准输出
2u错误输出
TYPE:文件类型,如DIR(目录)、REG(基本文件)等
DEVICE:指定磁盘的名称
SIZE/OFF:文件的大小
NODE:索引节点(文件在磁盘上的标识)
NAME:打开文件的确切名称
(2)exec 8< io.txt
创建一个io.txt,里面随便打个1234,
将操作符8指向标准输入,读输入流,将io.txt 中的内容作为exec的标准输入.
可以看到 FD:8r (8数组下标,r只读模式) , OFFSET:0t4:(文件指针读取偏移量),node:786508(文件的inode号,系统唯一可以找到该文件)
(3)查看系统限制最大fd数
#cat /proc/sys/fs/file-nr #输出:9216 0 6553600 (已分配fd,已分配但未使用,最大fd数)
(4)单进程限制打开最大fd数
#ulimit -n #输出:1048576 (刚安装系统默认1024,大小设置取决内存。太小容易:too many open files)
4.回到第1个问题为什么要open,close
(1)open?
操作系统需要检查这个文件是否真正存在,要检查当前进程还有没有空闲的fd,没空闲的直接:too many open files了。存在fd需要和它做映射。操作系统还有检查你是否有权限,有哪些可操作权限等等
(2)close?
fd_array:大小是有限制的,系统的资源有限。假如完成业务程序退出了,fd一直不关,那这个fd一值占着不放。一开始fd_array空闲数量充足程序还没问题,open多了的时候,很快就会提示:too many open files了
可以用#cat /proc/sys/fs/file-nr #输出:9216 0 6553600 (已分配fd,已分配但未使用,最大fd数)查看是否已分配完了。
可以用#ls -l /proc/进程PID/fd/ ,进入具体的进程看看都打开了哪些进程,会有详细信息记录。
5.fd相关面试题,判断一下对错,可以思考一下再看答案
(1)两个进程可以同时读取一个文件,分别产生生成两个独立的fd?
(2)两个进程可以任意对文件进行读写操作,操作系统并不保证写的原子性?
(3)进程可以通过系统调用对文件加锁,从而实现对文件内容的保护?
(4)任何一个进程删除该文件时,另外一个进程会立即出现读写失败?
(5)两个进程可以分别读取文件的不同部分而不会相互影响?
(6)一个进程对文件长度和内容的修改另外一个进程可以立即感知?
(7)一个进程不能有两个8,两个进程可以分别有8,同一进程内的FD相互独立,操作系统内,进程之间是严重隔离的?
1)两个进程可以同时读取一个文件,分别产生生成两个独立的fd?(对,两个fd_array,各管各的) 2)两个进程可以任意对文件进行读写操作,操作系统并不保证写的原子性?(对,因为写的原子性应该是由锁来控制) 3)进程可以通过系统调用对文件加锁,从而实现对文件内容的保护?(对,锁互斥) 4)任何一个进程删除该文件时,另外一个进程会立即出现读写失败?(错。并不会立即出现读写失败,一般删除都是文件索引,一个线程执行删除操作,只要另一个线程不退出,就可以继续对该文件进行操作,文件变更后fd后面会标记(deleted)一旦退出才找不到该文件的索引节点而报错) 5)两个进程可以分别读取文件的不同部分而不会相互影响(对,fd指针偏移量是独立的) 6)一个进程对文件长度和内容的修改另外一个进程可以立即感知(对,文件删除或修改后fd后面会标记(deleted)) 7)一个进程不能有两个8,两个进程可以分别有8,同一进程内的FD相互独立,操作系统内,进程之间是严重隔离的(对) |
二、inode是什么
前面有说的,fd会和inode号做映射,那inode是什么?inode号又是什么?
1、要理解inode,要从文件储存说起
我们都知道,文件持久化存储最后都是要落在磁盘上的。我们看看一个图(本图是网上摘得,画得真好)
(1)磁盘存储单位:最小单位扇区
cylinder:柱面
magnetic layer:磁层(磁盘多个盘面)
track:磁道(分若干弧段)
sector:扇区(弧段,512字节=0.5kb)//最小单位
(2)文件存储单位:块
block:块(连续8扇=4kb) (一个字节=8位) ,如果读写文件一个个扇区来,效率太低了,所以文件会8扇一起来刷。
(3)读写起始位置
disk read-and-write heads:磁头,磁头外缘从0开始,写满外圈再往内圈写
(4)查找位置:先柱面-在盘面-扇区
先找到在哪圈,哪层,然后磁盘转到具体扇区,开始转圈读写
既然文件数据是存储在磁盘中,那谁来记录这些数据在哪个圈层?占了多少块?是谁创建的?有什么权限?那是不是还要有个东西来存储。答案就是inode结构体
2、inode结构体
struct inode {
umode_t i_mode;//文件的访问权限(eg:rwxrwxrwx)
unsigned short i_opflags;
kuid_t i_uid;//inode拥有者id
kgid_t i_gid;//inode拥有者组id
unsigned int i_flags;//inode标志,可以是S_SYNC,S_NOATIME,S_DIRSYNC等
#ifdef CONFIG_FS_POSIX_ACL
struct posix_acl *i_acl;
struct posix_acl *i_default_acl;
#endif
const struct inode_operations *i_op;//inode操作
struct super_block *i_sb;//所属的超级快
/*
address_space并不代表某个地址空间,而是用于描述页高速缓存中的页面的一个文件对应一个address_space,一个address_space与一个偏移量能够确定一个一个也高速缓存中的页面。i_mapping通常指向i_data,不过两者是有区别的,i_mapping表示应该向谁请求页面,i_data表示被改inode读写的页面。
*/
struct address_space *i_mapping;
#ifdef CONFIG_SECURITY
void *i_security;
#endif
/* Stat data, not accessed from path walking */
unsigned long i_ino;//inode号
/*
* Filesystems may only read i_nlink directly. They shall use the
* following functions for modification:
*
* (set|clear|inc|drop)_nlink
* inode_(inc|dec)_link_count
*/
union {
const unsigned int i_nlink;//硬链接个数
unsigned int __i_nlink;
};
dev_t i_rdev;//如果inode代表设备,i_rdev表示该设备的设备号
loff_t i_size;//文件大小
struct timespec i_atime;//最近一次访问文件的时间
struct timespec i_mtime;//最近一次修改文件的时间
struct timespec i_ctime;//最近一次修改inode的时间
spinlock_t i_lock; /* i_blocks, i_bytes, maybe i_size */
unsigned short i_bytes;//文件中位于最后一个块的字节数
unsigned int i_blkbits;//以bit为单位的块的大小
blkcnt_t i_blocks;//文件使用块的数目
#ifdef __NEED_I_SIZE_ORDERED
seqcount_t i_size_seqcount;//对i_size进行串行计数
#endif
/* Misc */
unsigned long i_state;//inode状态,可以是I_NEW,I_LOCK,I_FREEING等
struct mutex i_mutex;//保护inode的互斥锁
//inode第一次为脏的时间 以jiffies为单位
unsigned long dirtied_when; /* jiffies of first dirtying */
struct hlist_node i_hash;//散列表
struct list_head i_wb_list; /* backing dev IO list */
struct list_head i_lru; /* inode LRU list */
struct list_head i_sb_list;//超级块链表
union {
struct hlist_head i_dentry;//所有引用该inode的目录项形成的链表
struct rcu_head i_rcu;
};
u64 i_version;//版本号 inode每次修改后递增
atomic_t i_count;//引用计数
atomic_t i_dio_count;
atomic_t i_writecount;//记录有多少个进程以可写的方式打开此文件
const struct file_operations *i_fop; /* former ->i_op->default_file_ops */
struct file_lock *i_flock;//文件锁链表
struct address_space i_data;
#ifdef CONFIG_QUOTA
struct dquot *i_dquot[MAXQUOTAS];//inode磁盘限额
#endif
/*
公用同一个驱动的设备形成链表,比如字符设备,在open时,会根据i_rdev字段查找相应的驱动程序,并使i_cdev字段指向找到的cdev,然后inode添加到struct cdev中的list字段形成的链表中
*/
struct list_head i_devices;,
union {
struct pipe_inode_info *i_pipe;//如果文件是一个管道则使用i_pipe
struct block_device *i_bdev;//如果文件是一个块设备则使用i_bdev
struct cdev *i_cdev;//如果文件是一个字符设备这使用i_cdev
};
__u32 i_generation;
#ifdef CONFIG_FSNOTIFY
//目录通知事件掩码
__u32 i_fsnotify_mask; /* all events this inode cares about */
struct hlist_head i_fsnotify_marks;
#endif
#ifdef CONFIG_IMA
atomic_t i_readcount; /* struct files open RO */
#endif
//存储文件系统或者设备的私有信息
void *i_private; /* fs or device private pointer */
}
3、一个文件一个inode结构体,里面对应一个系统唯一inode号
struct inode的结构体就是存储文件属性的结构体。注意inode只是存储文件的属性,文件属性和文件存储的内容是分开存储的。
硬盘格式化的时候,操作系统自动将硬盘分成两个区域。一个是数据区,存放文件数据;另一个是inode区(inode table),存放inode所包含的信息。
inode结构体很多数据,有兴趣可以详细研读,我们现在只关心(unsigned long i_ino;//inode号):一个文件一个inode结构体,里面对应一个系统唯一inode号。
4、查看文件inode信息
#stat rongshu.txt
file:文件名
size:大小
blocks:文件使用的数据块总数 8
io blocks:IO块大小4096
device:fd01h/64769d:磁盘位置
inode:786486:索引节点(这里看到每个文件都有一个inode号)
link:链接数,即有多少文件名指向这个inode
5、fd的node和inode struct里面的inode
细心的你可能发现,fd的node和inode struct里面的inode都是一个数字,名字也是类似,那是不是同一个东西?答案:是的,是同一个东西,可以自己验证
#exec 8< io.txt
#lsof -p $$
#stat io.txt
6.inode大小
(1)#df -i
(2)一般是128字节或256字节
7.inode是VFS使用的一个对象
(1)刚有说到,inode只是文件的属性,和内容是分开的,实际上inode是VFS使用的一个对象,是linux虚拟文件系统,挂载磁盘真实文件而创建的一个东西。
索引节点有两种:
一种是这里所说的VFS索引节点,存在内存中
一种是具体文件系统的索引节点,存在于磁盘上(可以理解为文件存储块的起始位置)
(2)打开一个文件时,操作系统就会把对应的文件的inode的部分属性摘录下来放在内存里,存在fd_array中
fd_array,可以理解是个二维数组,key=fd,value=node,node是inode号,这时返回个fd给用户,就可以操作文件了
(3)操作文件的时候,就可以快速通过fd找到node(inode号),通过inode号可以找到文件具体位置
三、了解fd有啥用
最后了解文件描述符有啥用呢?我理解
1.我们要做系统性能优化,I/O就是很重要的一块,I/O又分文件I/O,网络I/O。要想理解操作系统I/O工作原理,文件描述符是第一个要掌握的。这里是为后面学习I/O知识打基础。
2.理解这个更好理解操作系统抽象统一的思想,vfs就是个很好的例子,万物可以挂载。内核通过fd就可以高效管理系统资源
3.扩展:操作文件调用open可以拿到一个fd,启动服务调用socket函数可以拿到一个fd,进程间通信调用pipe可以拿到2个fd。
关于I/O学习是很重要的,文件描述符就写到这。