Linux 文件描述符与文件系统

  • 列表内容

文件描述符

Linux下一切皆文件 文件描述符是为了高效的管理已经被打开的文件而设计的 文件描述符作为操作文件的句柄 在Linux 下一切I/O操作的系统调用都是通过文件描述符操作的。文件描述符是一个非负整数,Linux下进程要访问一个文件就必须拿到该文件的文件描述符。

每一个进程控制块结构体(PCB)中中都有一个指向 file_struct结构体的指针。
file_struct结构体中有一个文件描述符表 这个文件描述符表实际上是一个指针数组 而这个指针数组中存放的是file结构体 的指针

每个进程有进程控制块PCB
file结构体实际上相当于 每个文件的控制块 描述着一个文件的属性信息。

文件描述符是连续的非负整数 本质上是file_struct结构体中只想file结构体指针的数组的下标。

这里写图片描述

这里写图片描述

如何获取文件描述符

1.子进程从父进程继承文件描述符 文件描述符对于每一个进程是唯一的,每个进程都有一张文件描述符表,用于管理文件描述符。当使用fork创建子进程的话,子进程会获得父进程所有文件描述符的副本,这些文件描述符在执行fork时打开。 父子进程拥有同样的文件描述符表 意味着可以访问同一个文件 每个进程是在自己的地址空间内运行 他们如果要相互实现通信 就得有在各自的文件描述符表的同一个文件描述符表示同一个文件 (公共资源) 通过这个中介你读我写 你写我读来事现通信。

  1. open() create() 系统调用

文件描述符与打开的文件之间的关系

每一个文件描述符对应一个打开的文件 同一个文件可以被多个文件描述符表示 相同的文件可以被不同的进程打开,也可以在同一个进程中被打开多次。系统为每一个进程维护了一个文件描述符表,该表的值从0开始,所以在不同的进程中会看到数字相同的文件描述符,这种情况下的相同的文件描述符有可能指向同一个文件,也有可能指向不同的文件。

与文件描述符有关的部分系统调用。

open函数

       #include <sys/types.h>
       #include <sys/stat.h>
       #include <fcntl.h>

       int open(const char *pathname, int flags);
       int open(const char *pathname, int flags, mode_t mode);

open函数的参数 :
第一个参数是要打开的文件的路径
第二个参数是标志位 标识出打开文件的方式
O_RDONLY 以只读方式打开
O_WRONLY 以只写方式打开
O_RDWR 以可读可写方式打开
O_APPEND 以追加写方式打开

O_CREAT 如果该文件不存在则创建它 这是需要填入第三个参数mode

第三个参数mode是文件的权限 用一个数字表式文件的拥有者 所属组 和其他 三种‘人’ 对文件的读写 执行权限。

open函数的返回值是所打开文件的文件描述符 失败返回-1 errno自动被置。

close函数

 #include <unistd.h
 int close(int fd);

close用来关闭一个文件 参数是要关闭的文件的文件描述符
成功返回值 0 失败返回 -1 并自动置errno。

read write函数

 #include <unistd.h>

ssize_t read(int fd, void *buf, size_t count);
 #include <unistd.h>

 ssize_t read(int fd, void *buf, size_t count);

read write函数 三个参数分别是

  1. 从哪个文件描述符标识的文件 读 写。
  2. 写 读 到哪个文件描述符中。
  3. 写 读 几个字节。

返回值是:
成功返回读写的字节数 失败返回-1 并置 errno

以上几个函数都是系统调用 fwrite fopen fread 这些C的库函数 封装了这些系统调用接口。

C库中的文件操作函数中的返回值和参数有 FILE*
C库中的FILE结构体有什么信息呢?

dup 和 dup2 系统调用

dup:复制文件描述符,返回没有使用的文件描述符中的最小编号。
dup2:由用户指定返回的文件描述符的值,用来重新打开或重定向一个文件描述符。

#include<stdio.h>
#include<string.h>
#include<sys/types.h>

#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>


int main()
{
   const char* msgg = "fwrite, running!\n";
   printf("printf running ...I\n");
   const char* msgg1 = "write, running!\n";
   fwrite(msgg, 1, strlen(msgg), stdout);
   write(1,msgg1, strlen(msgg1));
   pid_t id = fork();
    if(id == 0 ){
        printf("I am child!\n");
    }else{
        printf("I am father!\n");
    }
   return 0;
}

这里写图片描述

请注意 myfile.c 代码输出到标准输出 和重定向到文件中 有不一样的效果
在重定向后 fwrite函数的写操作在文件中写了两遍 而write函数在文件中只写了一边 这是为什么?

C的库函数写到显示器是行缓冲的 写到文件中是行缓冲的
fwrite函数是自带缓冲区的 当重定向到文件时 缓冲方式变成了全缓冲
fork()后子进程写时拷贝拷贝父进程内存数据 包括缓冲区数据 当父进程退出时要刷新缓冲区 这时子进程拷贝了一份新的数据 子进程退出时这部分数据再次被刷新到了文件中

write系统调用不提供缓冲区 调用结束后 所以父进程没有write的数据 创建出的子进程也没有 故只输出到文件一次。

(这里所说的缓冲区是用户级缓冲区)

从这个例子可以看书 C库中的 FILE结构体中至少有两个东西
1. 文件描述符
2. 缓冲区

FILE 结构体:

struct _IO_FILE {
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags

/* The following pointers correspond to the C++ streambuf protocol. */
/* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
//以下是封装的缓冲区
char* _IO_read_ptr; /* Current read pointer */
char* _IO_read_end; /* End of get area. */
char* _IO_read_base; /* Start of putback+get area. */
char* _IO_write_base; /* Start of put area. */
char* _IO_write_ptr; /* Current put pointer. */
char* _IO_write_end; /* End of put area. */
char* _IO_buf_base; /* Start of reserve area. */
char* _IO_buf_end; /* End of reserve area. */
/* The following fields are used to support backing up and undo. */
char *_IO_save_base; /* Pointer to start of non-current get area. */
char *_IO_backup_base; /* Pointer to first valid character of backup area */
char *_IO_save_end; /* Pointer to end of non-current get area. */

struct _IO_marker *_markers;

struct _IO_FILE *_chain;

int _fileno;
  //这里就是文件描述符
#if 0
int _blksize;
#else
int _flags2;
#endif
_IO_off_t _old_offset; /* This used to be _offset but it's too small. */

#define __HAVE_COLUMN /* temporary */
/* 1+column number of pbase(); 0 is unknown. */
unsigned short _cur_column;

signed char _vtable_offset;
char _shortbuf[1];

/* char* _save_gptr; char* _save_egptr; */

_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};

文件描述符的分配规则 与 重定向

Linux进程 在默认情况下 再一开始就会默认打开三个文件描述符 0 1 2
也就是 file结构体中文件描述符表file_array[] 的头三个下标

这三个文件描述符 0 1 2 对应的三个文件 键盘 显示器 显示器 (Linux下一切皆文件 ) 对应C语言中的三个流 stdin stdout stderror 三个流(FILE* 类型)
正常输出到显示器上 叫stdout标准输出 。
出错信息显示到显示器上交 stderror标准错误。

#include<stdio.h>
#include<string.h>
#include<sys/types.h>

#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>


int main()
{
   close(1);
   int fd = open("./print_file",O_RDWR|O_CREAT,0755);
   printf("fd = %d\n",fd);
   printf("helloworld\n");
   fflush(stdout);
   return 0;
}

这段代码一上来关闭了 默认打开的标准输出文件描述符 1 然后打开了文件print_file 之后调用了printf函数 这是我们发现 ./myfile后显示屏上无显示 再查看printf_file文件 里面显示fd = 1 和helloword
请注意这里调用了fflush函数刷新了stdout 的输出缓冲区 才使得字符写进了 printf_file

这里说明
1. printf函数是向文件描述符为 1 的文件写的
2. 文件描述符的分配规则是从零开始找第一个未被使用的文件描述符来分配的。

这里实现了printf函数的输出重定向。

这里写图片描述

自己模拟一个有重定向功能的命令行解释器myshell

根据文件描述符的分配规则 和exec系列函数的概念模拟一个有重定向功能的shell命令行解释器

#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<unistd.h>
#include<stdio.h>
#include<sys/stat.h>
#include<sys/fcntl.h>
#include<string.h>
#include<ctype.h>

int main(){
    while(1){
        printf("[ym@localhost process myshell]$ ");
        fflush(stdout);//刷新输出缓冲区  打印命令行解释器的头部信息
        char buf[1024];//定义一个缓冲区  用来存储从键盘输入的命令
        ssize_t s = read(0,buf,sizeof(buf)-1);
        if(s > 0){
            buf[s-1] = 0;//确保缓冲区最后的‘\0’
            //printf("%s\n", buf);
        }
        char* start = buf;
        char* _argv[32] = {NULL};
        int i = 0;//将存储命了的缓冲区中的命令参数存入_argv[]  空格填上 ‘\0’
        while(*start){
            while(*start && isspace(*start)){
                *start = 0;
                start++;
            }
            _argv[i++] = start;
            while(*start && !isspace(*start)){
                start++;
            }
        }
        _argv[i] = NULL;//_argv[] 要作为execv的参数 所以末尾必须加NULL
        pid_t id = fork();//依照shell的原理创建子进程
        if(id == 0){
            int flag = 0;
            int i = 0;
            for( ; _argv[i] != NULL; ++i ){//寻找 命令行参数中的 ‘>’ 重定向符号
                if(strcmp(">", _argv[i]) == 0){
                    flag = 1;//flag设置为1 表示有重定向符号
                    break;
                }
            }
            int copyfd;
            _argv[i] = NULL;//要作为execv的参数 _argv[i]要设置为 NULL
            if(flag){
                close(1);//关闭标准输出缓冲区
                int fd = open(_argv[i + 1], O_RDWR | O_CREAT, 0755); //此时fd为1
               //copyfd = dup2(1, fd); 
           }
           execvp(_argv[0],_argv);//被替换的程序 如果有重定向 因为PCB没有变 file结构体没有变  文件描述符没有变
           //此时原本默认的输出到文件描述符为1的文件 已经不是显示屏  而是新打开的一个文件
           //if(flag){
           //   close(1);
           //   dup2(copyfd, 1);
           //}
           exit(1);//进程退出时会对自己打开的文件描述符关闭   下一次while(1)的循环开始时 重新创建子进程 默认打开 0 1 2 stdin stdout stderror
       }else{
           pid_t ret = waitpid(id, NULL, 0);
           if(ret > 0){
              // printf("wait child success!\n");
           }
       }
   }
   return 0;
}

这里写图片描述

内核的文件操作结构体关系图   (转来的):
这里写图片描述

文件描述相关联的各个用户级和系统级结构体关系和作用
http://blog.csdn.net/captain_mxd/article/details/52153233
http://blog.csdn.net/lf_2016/article/details/54605651

文件系统

除了ls -l 命令可以为我们从磁盘中获取 文件夹中文件的信息
还有一个命令 stat 命令

这里写图片描述

如何stat命令显示的信息 ?
首先理解一下上面两个链接中提到的inode结点
这里写图片描述

文件系统将磁盘分成这几块?
超级块 :存放文件系统本身的结构信息
inode: 存放文件属性 如 文件大小 所有者 最近修改时间 存在哪个磁盘区块
数据区:一般被分为512k一块的磁盘区域 用来存储数据

touch命令创建一个文件都干了些什么呢?

  1. 内核先找到一个空闲的i节点 把要新建的文件初始信息填进去
  2. 存储文件数据 计算发现该文件存储需要 3 个盘块 比如这三个盘块是 100 200 300 则将内核缓冲区中的 数据一块一块地输出到磁盘对应的盘块上
  3. 记录分配情况 inode上的磁盘分布记录了上述 块列表
  4. 内核将 inode节点号和文件名 一起作为文件的入口 存储到目录文件中
    文件名和 inode节点的对应关系 把文件名和 文件的数据和属性对应起来

硬链接与软连接

至此 我们知道真正找到磁盘上文件的是inode节点 而不是文件名
其实 我们可以让多个文件名对应一个inode节点。
这就是硬链接

ln abc test.c ln命令为test.c文件定义一个硬链接
对于硬链接来说 其实是两个文件名公用一个inode节点 在目录文件中保存了这两个文件名和inode节点的关系
删除时 在目录文件中将inode 与一个文件名的记录删除 inode结点中保存的硬链接数减一 如果减到了零 则删除该文件

ln -s abc test.c 对test.c 文件搞一个abc这样的软连接
软连接实际上自己有另一个 inode节点 这个inode节点中对应的磁盘块保存的是test.c文件的路径。

这里写图片描述
软链接常用于大型工程中文件路径的指代。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章