文件I/O_02文件描述符fd

一、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学习是很重要的,文件描述符就写到这。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值