Linux:文件描述符fd

上篇文章:Linux:基础IO


目录

1.系统级别调用

1.1得到与open函数r模式相同的效果

2.文件描述符

2.1认识fd

从3开始的原因

为什么是连续的

为什么它可以表示文件

fd的本质

2.2文件描述符

2.2.1源码验证

2.2.2文件描述符分配规则

C函数封装和系统调用,fd之间的关系

那么这些语言为什么要做封装?直接系统调用不行吗?

2.2.3理解重定向

dup2专门进行重定向

输入重定向

3.优化自主shell命令行解释器

4.拓展

5.理解"一切皆文件"


书接上文

1.系统级别调用

1.1得到与open函数r模式相同的效果

使用的函数:

复制代码
int main(int argc, char *argv[])
{
    if(argc != 2)
    {
        printf("Usage: %s filename\n", argv[0]); 
        return 1;
    }
    int fd = open(argv[1], O_RDONLY);
    if(fd < 0)
    {
        perror("open");
        return 2;
    }
    
    char inbuffer[128];
    while(1)
    {
        ssize_t n = read(fd, inbuffer, sizeof(inbuffer)-1);
        if(n > 0)
        {
            inbuffer[n] = 0;
            printf("%s", inbuffer);
        }
        else if(n == 0)
        {
            printf("end of file!\n");
            break;
        }
        else 
        {
            perror("read");
            break;
        }
    }
    close(fd);
    return 0;
}

运行结果:

2.文件描述符

2.1认识fd

通过对open函数的学习,我们知道了文件描述符就是一个小整数:

复制代码
int main()
{
    int fd = open("log.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
    printf("fd: %d\n", fd);

    return 0;
}

运行结果:

在一个进程中,可以打开多个文件,那打开多个文件时,fd代表哪些数字呢?

复制代码
int main()
{
    int fda = open("loga.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
    int fdb = open("logb.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
    int fdc = open("logc.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);

    printf("fda : %d\n", fda);
    printf("fdb : %d\n", fdb);
    printf("fdc : %d\n", fdc);


    close(fda);
    close(fdb);
    close(fdc);
    return 0;
}

结果:

从3开始的原因

因为在创建程序/进程时,程序/进程会先创建标准输入输出流。(stdin:标准输入,键盘;stdout:标准输出,显示器;stderr:标准错误,显示器)

这里的FILE在c语言中是一种结构体,而这个FILE结构封装了fd,

验证:

复制代码
printf("stdin: %d\n", stdin->_fileno);
printf("stdout:%d\n", stdout->_fileno);
printf("stderr:%d\n", stderr->_fileno);

所以操作系统在系统层面,只认文件描述符。

为什么是连续的

推测:0123是它的数组下标

为什么它可以表示文件

文件被分为属性和内容,当你要访问磁盘时本质是将文件加载到内存中。那么打开文件的本质就是预加载文件,而一个进程可以打开多个文件,同时操作系统内部需要对多个文件进行管理。再通过下述图片的链接,操作系统对被打开的文件进行建模,转换为对链表的增删查改。

进程通过struct files_struct对被打开的文件进行管理:

文件描述符表的数组下标指向文件的数据,通过文件描述符可以对文件进行管理。

fd的本质

本质就是数组下标

复制代码
    printf("stdin: %d\n", stdin->_fileno);
    printf("stdout:%d\n", stdout->_fileno);
    printf("stderr:%d\n", stderr->_fileno);

    FILE *fp = fopen("log.txt", "w");
    printf("log.txt: %d\n", fp->_fileno); 

2.2文件描述符

2.2.1源码验证

2.2.2文件描述符分配规则

从最小的没有被使用的下标开始分配,分配下标需要再数组中连续:

复制代码
    close(0);
    int fda = open("loga.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
    int fdb = open("logb.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
    int fdc = open("logc.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);

    printf("fda : %d\n", fda);
    printf("fdb : %d\n", fdb);
    printf("fdc : %d\n", fdc);
复制代码
    close(2);
    int fda = open("loga.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
    int fdb = open("logb.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
    int fdc = open("logc.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);

    printf("fda : %d\n", fda);
    printf("fdb : %d\n", fdb);
    printf("fdc : %d\n", fdc);

但是当关闭数组下标1时:

复制代码
    close(1);
    int fda = open("loga.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
    int fdb = open("logb.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
    int fdc = open("logc.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);

    printf("fda : %d\n", fda);
    printf("fdb : %d\n", fdb);
    printf("fdc : %d\n", fdc);

显示器中没有显示,在loga.txt中倒是有显示。此时输出重定向

C函数封装和系统调用,fd之间的关系

只能用fd,只能是系统调用,而C++中,std::cout;std::cin;std::cerr这些类,也必定要封装fd。

像Python解释器,Java的虚拟机都是用C/C++完成的。所以也必定封装fd.

那么这些语言为什么要做封装?直接系统调用不行吗?

以C语言为例,此时他已经被封装好了。

那么在C语言在Linux中安装,它需要调用Linux系统调用,在Windows中安装,需要调用Windows系统调用。即使这两个系统调用不同,单用户只需要学习C语言封装的接口即可。底层的差异由C标准库解决。达到跨平台性!

语言为什么要跨平台性?一种平台的背后有着一大批开发者,最终为了争取开发者。

跨平台性的本质是打造语言的生态。

2.2.3理解重定向

为什么当关闭数组下标1时:

复制代码
    close(1);
    int fda = open("loga.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
    int fdb = open("logb.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
    int fdc = open("logc.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);

    printf("fda : %d\n", fda);
    printf("fdb : %d\n", fdb);
    printf("fdc : %d\n", fdc);

显示器中没有显示,在loga.txt中倒是有显示。此时输出重定向

原因:printf实际是通过stdout,而它对应的int fd = 1;

复制代码
    close(1);
    int fda = open("loga.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
    int fdb = open("logb.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
    int fdc = open("logc.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
    
    fprintf(stdout, "fda : %d\n", fda);
    fprintf(stdout, "fdb : %d\n", fdb);
    fprintf(stdout, "fdc : %d\n", fdc);
    
    printf("fda : %d\n", fda);
    printf("fdb : %d\n", fdb);
    printf("fdc : %d\n", fdc);

重定向本质就是1号下标指向的标准输出根据文件描述符的分配规则被改变为指向新打开的文件:

dup2专门进行重定向

重定向是将目标文件中文件描述符中的内容拷贝到被重定向的文件描述符1里面,类似于发生一种浅拷贝。

进行重定向都需要先关闭文件再打开文件,dup2 makes newfd be the copy of oldfd, closing newfd first if necessary, but note the following:

示例:

复制代码
    int fda = open("loga.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
    dup2(fda, 1);

    printf("hello printf\n");
    fprintf(stdout, "helo fprintf\n");

它的效果与echo相似:

输入重定向
复制代码
    int fda = open("loga.txt", O_RDONLY);
    dup2(fda, 0);

    int a = 0;
    float f = 0.0f;
    char c = 0;
    scanf("%d %f %c", &a, &f, &c);

    printf("%d, %f, %c\n", a, f, c);

此种操作相当于:

3.优化自主shell命令行解释器

相关文章:Linux:自主shell命令行解释器附源码

优化Execute函数:

复制代码
void Execute()
{
    pid_t id = fork();
    if(id < 0)
    {
        perror("fork");
        return;
    }
    else if(id == 0)
    {
        // child
        // 程序替换
        //
        // 重定向
        if(redir_type == IN_REDIR)
        {
            int fd = open(redir_filename, O_RDONLY);
            (void)fd;
            dup2(fd, 0);
        }
        else if(redir_type == OUT_REDIR)
        {
            int fd = open(redir_filename, O_CREAT | O_WRONLY | O_TRUNC, 0666);
            (void)fd;
            dup2(fd, 1);
        }
        else if(redir_type == APP_REDIR)
        {
            int fd = open(redir_filename, O_CREAT | O_WRONLY | O_APPEND, 0666);
            (void)fd;
            dup2(fd,1);
        }
        else 
        {
            // nothing
        }
        execvp(argv[0], argv);
        exit(1);
    }
    else{
        // father
        int status = 0;
        pid_t rid = waitpid(id, &status, 0);
        (void)rid;
        lastcode = WEXITSTATUS(status);
    }
}

新增Redir函数:

复制代码
void Redir()
{
    // 核心目标 "ls -a -l >> > < filename"
   char *start = commandline;
   char *end = commandline+strlen(commandline);
   while(start < end)
   {
       // > >> <
       if(*start == '>')
       {
            if(*(start+1) == '>')
            {
                // 追加重定向
                // ls -a -l >> filename 
                redir_type = APP_REDIR;
                *start = '\0';
                start += 2;
                CLEAR_LEFT_SPACE(start);
                redir_filename = start;
                break;
            }
            else 
            {
                // 输出重定向
                redir_type = OUT_REDIR;
                *start = '\0';
                start++;
                CLEAR_LEFT_SPACE(start);
                redir_filename = start;
                break;
            }
       }
       else if(*start == '<')
       {
            // 输入重定向
            redir_type = IN_REDIR;
            *start = '\0';
            start++;
            CLEAR_LEFT_SPACE(start);
            redir_filename = start;
            break;
       }
       else 
       {
           start++;
       }
   }
}

4.拓展

每一个进程都会打开标准输入,标准输出,标准错误,0,1,2

为什么要打开?为什么是他们?怎么做到的?

程序的本质是加工处理用户数据的,而程序要有办法获得数据,一般主要获得数据的途径为键盘获取和显示器打印,支持debug,0,1,stdin,stdout

为什么要有stderr?

Linux/Unix 程序默认有两个输出通道,完全独立:

  1. 标准输出 stdout(文件描述符 1)正常运行的普通信息printfcout 默认往这里输出。
  2. 标准错误 stderr(文件描述符 2)报错、警告、异常信息perrorcerr 默认往这里输出。

这两个流本来是分开的,只是默认都打印在终端,看起来混在一起。

运行结果:

实际上,直接使用 > 是一种简写,本质是:

是怎么默认打开三项标准的?

当一个进程打开若干文件,如果这个进程创建子进程,那么当前进程的文件描述符表要给子进程拷贝一份(浅拷贝,指针部分也要拷贝),所以子进程从来没有打开过三项标准,都是从bash父进程继承过来的。所以会默认打开。

5.理解"一切皆文件"

在windows中是文件的东西,它们在linux中也是文件;其次一些在windows中不是文件的东西,比如进程、磁盘、显示器、键盘这样硬件设备也被抽象成了文件,你可以使用访问文件的方法访问它们获得信息;甚至管道,也是文件;将来我们要学习网络编程中的socket这样的东西,使用的接口跟文件接口也是一致的。

这样做最明显的好处是,开发者仅需要使用一套API和开发工具,即可调取Linux系统中绝大部分的资源。举个简单的例子,Linux中几乎所有读(读文件,读系统状态,读PIPE)的操作都可以用read 函数来进行;几乎所有更改(更改文件,更改系统参数,写 PIPE)的操作都可以用 write 函数来进行。

之前我们讲过,当打开一个文件时,操作系统为了管理所打开的文件,都会为这个文件创建一个file结构体, 该结构体定义在 /usr / src/kernels/3.10.0-1160.71.1.el7.x86_64/include/linu×/fs.h 下, 以下展示了该结构部分我们关系的内容:

复制代码
struct file {
 ...
 
struct inode *f_inode; /* cached value */
 const struct file_operations *f_op;
 
 ...
 
 atomic_long_t f_count; // 表⽰打开⽂件的引⽤计数,如果有多个⽂件指针指
向它,就会增加f_count的值。 
 unsigned int f_flags; // 表⽰打开⽂件的权限 
 fmode_t f_mode; // 设置对⽂件的访问模式,例如:只读,只写等。所
有的标志在头⽂件<fcntl.h> 中定义 
 loff_t f_pos; // 表⽰当前读写⽂件的位置 
 
 ...
 
} __attribute__((aligned(4))); /* lest something weird decides that 2 is OK 
*/

值得关注的是 struct file 中的 f_op 指针指向了一个 file_operations 结构体,这个结构体中的成员除了struct module*owner 其余都是函数指针。 该结构和 struct file 都在fs.h下。

复制代码
struct file_operations {
 struct module *owner; 
 //指向拥有该模块的指针; 
 loff_t (*llseek) (struct file *, loff_t, int); 
 //llseek ⽅法⽤作改变⽂件中的当前读/写位置, 并且新位置作为(正的)返回值.  
 ssize_t (*read) (struct file *, char __user *, size_t, loff_t *); 
 //⽤来从设备中获取数据. 在这个位置的⼀个空指针导致 read 系统调⽤以 -
EINVAL("Invalid argument") 失败. ⼀个⾮负返回值代表了成功读取的字节数( 返回值是⼀个 
"signed size" 类型, 常常是⽬标平台本地的整数类型). 
 ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
 //发送数据给设备. 如果 NULL, -EINVAL 返回给调⽤ write 系统调⽤的程序. 如果⾮负, 
返回值代表成功写的字节数. 
 ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, 
loff_t);
 //初始化⼀个异步读 -- 可能在函数返回前不结束的读操作. 
 ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned 
long, loff_t);
 //初始化设备上的⼀个异步写. 
 int (*readdir) (struct file *, void *, filldir_t);
 //对于设备⽂件这个成员应当为 NULL; 它⽤来读取⽬录, 并且仅对**⽂件系统**有⽤. 
 unsigned int (*poll) (struct file *, struct poll_table_struct *);
 int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
 long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
 long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
 int (*mmap) (struct file *, struct vm_area_struct *);
 //mmap ⽤来请求将设备内存映射到进程的地址空间. 如果这个⽅法是 NULL, mmap 系统调⽤
返回 -ENODEV. 
 int (*open) (struct inode *, struct file *);
 //打开⼀个⽂件 
 int (*flush) (struct file *, fl_owner_t id);
 //flush 操作在进程关闭它的设备⽂件描述符的拷⻉时调⽤; 
 int (*release) (struct inode *, struct file *);
 //在⽂件结构被释放时引⽤这个操作. 如同 open, release 可以为 NULL. 
 int (*fsync) (struct file *, struct dentry *, int datasync);
 //⽤⼾调⽤来刷新任何挂着的数据. 
 int (*aio_fsync) (struct kiocb *, int datasync);
 int (*fasync) (int, struct file *, int);
 int (*lock) (struct file *, int, struct file_lock *);
 //lock ⽅法⽤来实现⽂件加锁; 加锁对常规⽂件是必不可少的特性, 但是设备驱动⼏乎从不实
现它. 
 ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, 
int);
 unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned 
long, unsigned long, unsigned long);
 int (*check_flags)(int);
 int (*flock) (struct file *, int, struct file_lock *);
 ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t 
*, size_t, unsigned int);
 ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, 
size_t, unsigned int);
 int (*setlease)(struct file *, long, struct file_lock **);
};

file_operation就是把系统调用和驱动程序关联起来的关键数据结构,这个结构的每一个成员都对应着一个系统调用。读取 file_operation 中相应的函数指针,接着把控制权转交给函数,从而完成了Linux设备驱动程序的工作。

上图中的外设,每个设备都可以有自己的read、write,但一定是对应着不同的操作方法!!但通过struct file 下 file_operation 中的各种函数回调,让我们开发者只用file便可调取 Linux 系统中绝大部分的资源!!这便是"linux下一切皆文件"的核心理解。

本章完。

相关推荐
未既1 小时前
逻辑卷挂载磁盘操作命令
linux·运维·服务器
那就回到过去2 小时前
拥塞管理和拥塞避免
运维·服务器·网络·网络协议·tcp/ip·ensp
李斯维2 小时前
安装 Arch Linux 到 VMware Workstation 的完全指南
linux
未来之窗软件服务2 小时前
服务器运维(三十六)日志分析nginx日志工具—东方仙盟
运维·服务器·服务器运维·仙盟创梦ide·东方仙盟
香蕉你个不拿拿^3 小时前
Linux粘滞位和文件,目录权限
linux·运维·服务器
木子欢儿3 小时前
Debian挂载飞牛OS创建的RAID分区和Btrfs分区指南
运维·debian
2401_858286113 小时前
OS52.【Linux】System V 共享内存(1)
linux·运维·服务器·共享内存
智能零售小白白4 小时前
零售会员营销自动化:标签体系与精准触达的技术实现
运维·自动化·零售
wbs_scy4 小时前
Linux 实战:从零实现动态进度条(含缓冲区原理与多版本优化)
linux·运维·服务器