Linux文件接口及文件描述符

本文详细介绍了如何使用系统接口进行文件操作,包括文件的打开、读取、写入及关闭等核心过程。特别关注了open函数的参数配置,write与read函数的使用细节,以及文件描述符的概念和作用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

系统文件I/O

操作文件,除了C接口(当然,C++也有接口,其他语言也有),我们还可以采用系统接口来进行文件访问

写文件:

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

int main()
{
    umask(0);
    int fd = open("myfile", O_WRONLY|O_CREAT, 0644);
    if(fd < 0)
    {
        perror("open");
        return 1;
    }
    
    int count = 5;
    const char *msg = "hello bit!\n";
    
    int len = strlen(msg);
    while(count--)
    {
    	write(fd, msg, len);//fd: 后面讲, msg:缓冲区首地址, len: 本次读取,期望写入多少个字节的数据。 返回值:实际写了多少字节数据
    }
    
    close(fd);
    return 0;
}

读文件:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{
    int fd = open("myfile", O_RDONLY);
    if(fd < 0)
    {
    	perror("open");
        return 1;
    }
    
    const char *msg = "hello bit!\n";
    char buf[1024];
    while(1)
    {
    	ssize_t s = read(fd, buf, strlen(msg));//类比write
        if(s > 0)
        {
            printf("%s", buf);
        }
        else
        {
            break;
        }
        
    }
    close(fd);
    return 0;
}

接口介绍

open man open

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);

  • pathname: 要打开或创建的目标文件

  • flags: 打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行“或”运算,构成flags。
    参数:

    1. O_RDONLY: 只读打开
    2. O_WRONLY: 只写打开
    3. O_RDWR : 读,写打开
      这三个常量,必须指定一个且只能指定一个
    4. O_CREAT : 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限
    5. O_APPEND: 追加写
  • 返回值:
    成功:新打开的文件描述符
    失败:-1

mode_t理解:直接 man 手册,比什么都清楚。
open 函数具体使用哪个,和具体应用场景相关,如目标文件不存在,需要open创建,则第三个参数表示创建文件的默认权限,否则,使用两个参数的open

write read close lseek 等都属于系统接口,类比C文件相关接口

接口的使用

open

重点解释第二个参数flags的使用

第二个参数有O_RDONLY,O_WRONLY,O_RDWR ,O_CREAT : 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限。O_TRUNC:覆盖写,O_APPEND: 追加写

前三个参数好理解,我们就不具体讲解了

关于O_CREAT:

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

int main()
{
  int fd = open("log.txt", O_WRONLY|O_CREAT, 0644);
  if (fd < 0)
  {
    perror("open file");
    return 1;
  }

  char*buf = "hello world\n";
  write(fd, buf, strlen(buf));//向我们新建的log.txt中写入hello world

  return 0;
}

结果:

image-20211004221503576

如果我们再执行一次,log.txt中还是只有一个hello world,因为此时默认的是覆盖写入:

image-20211004221711867

只不过这是隐式的,我们也可以显式地添加选项O_TRUNC,该选项就表示覆盖写入。

如果我们想接在log.txt已有的内容后面继续写入,就可以使用选项O_APPEND,表示追加写入

将上面的代码中的open选项稍微修改一下

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

int main()
{
  int fd = open("log.txt", O_WRONLY|O_CREAT|O_APPEND, 0644);
  if (fd < 0)
  {
    perror("open file");
    return 1;
  }

  char*buf = "hello world\n";
  write(fd, buf, strlen(buf));//向我们新建的log.txt中写入hello world

  return 0;
}

多次运行该程序,结果:

image-20211004222212277

其实,O_RDONLY,O_WRONLY…这些选项,本质上是宏,表示某个数字,这些数字转换成二进制数字后,所有的位里面只有一个1,也就表示某个功能(权限)的开启,我们看到使用多个功能时,就要用按位或|运算符,将对应的位都置为1,这样函数才知道有哪些功能可以使用。

write

函数原型

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

fd表示写入文件的文件描述符,buf表示指向的空间,count表示我们想让文件读取的字节数,返回值表示实际读取的字节数。

read 和 close就类比write 和 open理解就可以了

open函数返回值

在认识返回值之前,先来认识一下两个概念: 系统调用 和 库函数

  • 上面的 fopen fclose fread fwrite 都是C标准库当中的函数,我们称之为库函数(libc)。

  • 而, open close read write lseek 都属于系统提供的接口,称之为系统调用接口

  • 回忆一下我们讲操作系统概念时,画的一张图

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RnRwOSOX-1642676253595)(C:\Users\晏思俊\AppData\Roaming\Typora\typora-user-images\image-20211004113037174.png)]

    系统调用接口和库函数的关系一目了然。
    所以,可以认为,f#系列的函数,都是对系统调用的封装,方便二次开发。

open函数返回值:

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

int main()
{
  int fd = open("log.txt", O_WRONLY);
    
    //如果文件不存在就创建
  int fd1 = open("log1.txt", O_RDONLY|O_CREAT);
  int fd2 = open("log2.txt", O_RDONLY|O_CREAT);
  int fd3 = open("log3.txt", O_RDONLY|O_CREAT);
    
    //打开不存在的文件
  int fd4 = open("log4.txt", O_RDONLY);
  int fd5 = open("log5.txt", O_RDONLY);
    
  printf("%d\n", fd);
  printf("%d\n", fd1);
  printf("%d\n", fd2);
  printf("%d\n", fd3);
  printf("%d\n", fd4);
  printf("%d\n", fd5);
    
  close(fd);
  close(fd1);
  close(fd2);
  close(fd3);
  close(fd4);
  close(fd5);
    
  return 0;
}

image-20211004164845010

我们看到open的返回值在OS层面就是一个整数

文件描述符fd

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

但是我们打开的文件,返回的整数是从3开始而不是从1开始或0开始呢?

因为0,1,2三个整数,对应的三个C语言默认打开的文件,标准输入0,标准输出1,标准错误2

0 & 1 & 2

  • 0,1,2对应的物理设备一般是:键盘,显示器,显示器
    所以输入输出还可以采用如下方式 :

    #include <stdio.h>
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <fcntl.h>
    #include <string.h>
    
    int main()
    {
        char buf[1024];
        ssize_t s = read(0, buf, sizeof(buf));//从键盘上读取数据到buf中
    
        if(s > 0)//表示如果成功读到了数据
        {
        	buf[s] = 0;//将buf的最后一个元素设置为'\0'
        	write(1, buf, strlen(buf));//向标准输出流写
        	write(2, buf, strlen(buf));//向标准输入流写
        }
    
        return 0;
    }
    

我们仔细观察这些从0开始的整数fd,是不是很像数组的下标?

没错,这些整数,就是一个数组的下标,这个数组就是将打开的文件组织起来。

首先,一个进程是可以打开多个文件的,那么,打开了这么多文件,总要对这些文件进行管理,提到管理,我们就要想到:先描述,再组织。

对于打开的文件,我们用一个数组来进行管理,这个数组,是一个结构体指针数组file* fd_array[],里面存放的指针都指向我们已经打开的文件。这些文件都是从一个进程中打开的,那么就要将它们与进程联系起来,进程里面有一个指针,指向结构体files_struct,这个结构体里又包括了指针数组fd_array。因此,进程与打开的文件就联系起来了。

具体关系如下图:

image-20211004171545794

而现在知道,文件描述符就是从0开始的小整数。当我们打开文件时,操作系统在内存中要创建相应的数据结构来
描述目标文件。于是就有了file结构体。表示一个已经打开的文件对象。而进程执行open系统调用,所以必须让进
程和文件关联起来。每个进程都有一个指针*files, 指向一张表files_struct,该表最重要的部分就是包涵一个指针数
组,每个元素都是一个指向打开文件的指针!所以,本质上,文件描述符就是该数组的下标。所以,只要拿着文件
描述符,就可以找到对应的文件

浅层理解为什么Linux下一切皆文件

在file结构体中,是通过函数指针来对底层的硬件进行读写等操作的,底层有办法将对应的函数来针对不同的硬件进行操作。因此,从操作系统层来看,所有的对象都是由一个file结构体来描述的,中间就像一层虚拟层,模糊了视野,所以才说,Linux下一切皆文件。

image-20211101145255939

文件描述符的分配规则

直接看代码:

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

int main()
{
    int fd = open("myfile", O_RDONLY);
    if(fd < 0)
    {
        perror("open");
        return 1;
    }
    
    printf("fd: %d\n", fd);
    
    close(fd);
    return 0;
}

输出发现是 fd: 3
关闭0或者2,再看

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
    //关闭0
    close(0);
    //关闭2
    //close(2);
    int fd = open("myfile", O_RDONLY);
    if(fd < 0)
    {
    	perror("open");
    	return 1;
    }
    
    printf("fd: %d\n", fd);
    
    close(fd);
    return 0;
}

发现是结果是: fd: 0 或者 fd 2 .

可见,文件描述符的分配规则:在files_struct数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符

重定向

那如果关闭1呢?看代码:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
int main()
{
    close(1);//关闭显示器,空出1号位置
    
    //myfile就被放到了1号位置
    int fd = open("myfile", O_WRONLY|O_CREAT, 00644);
    if(fd < 0)
    {
    	perror("open");
    	return 1;
    }
    
    printf("fd: %d\n", fd);
    
    fflush(stdout);
    close(fd);
    exit(0);
}

此时,我们发现,本来应该输出到显示器上的内容,输出到了文件 myfile 当中,其中,fd=1。这种现象叫做输出重定向。常见的重定向有:>, >>, <

那重定向的本质是什么呢?

我们拿stdout解释,stdout本质是一个文件指针FILE*,也就是指向了一个结构体FILE,该结构体里有一个变量fileno,也就是文件描述符,stdout指向的结构体内的文件描述符是默认为1的,当我们close掉显示器,myfile被放入1号位置,此时printf是向stdout,也就是1号位置的文件输出内容的,但是我们在底层将1号位置的显示器文件改成了myfile,在代码层的stdout指针并不知情,也就是说,stdout此时是指向myfile的,所以printf不是向stdout自认为的显示器输出,而是向myfile中输出了。

当shell命令行(具体是bash)检测到重定向符号后(以">"为例),将1即标准输出关掉,打开的文件就放入了1的位置,这个操作是操作系统执行的。而stdout并不知情,所以默认还是将printf的内容输出给1号位置的文件,实际上是给我们重定向的文件输出了。

image-20211004175256185

缓冲区

看一段代码:

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


int main()
{
  //关闭显示器
  close(1);
    
  //log.txt的文件描述符就是1
  int fd = open("log.txt", O_WRONLY|O_CREAT|O_APPEND, 0644);
  if (fd < 0)
  {
    perror("open error");
    return 1;
  }

  char*buf1 = "hello write\n";
  char*buf2 = "hello printf\n";
  char*buf3 = "hello fprintf\n";
	
  write(1, buf1, strlen(buf1));//向文件描述符为1的文件中写入,也就是log.txt
  
    //向stdout打印buf2,实际上写到了log.txt中
  printf(buf2);
    //与printf同理
  fprintf(stdout, "%s", buf3);

  fork();
  fflush(stdout);
  
  return 0;
}

image-20211004225811439

修改一下代码,不关闭1号文件即显示器

#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("log.txt", O_WRONLY|O_CREAT|O_APPEND, 0644);
  if (fd < 0)
  {
    perror("open error");
    return 1;
  }

  char*buf1 = "hello write\n";
  char*buf2 = "hello printf\n";
  char*buf3 = "hello fprintf\n";

  write(1, buf1, strlen(buf1));
  printf(buf2);
  fprintf(stdout, "%s", buf3);

  fork();
  fflush(stdout);
  
  return 0;
}
image-20211004231516779

为什么出现了这种情况呢?

首先,我们要了解,显示器缓冲区的缓冲方式是行缓冲,也就是我们之前讲的;文件缓冲区的缓冲方式是全缓冲,也就是当缓冲区写满了才会刷新,或者强制刷新。

使用close,数据写到log.txt文件里,因为文件是全缓冲的,也就是printf和fprintf将打印数据放到了缓冲区,但是缓冲区没有满且没有强制刷新,所以log.txt里还没有这两行数据,当fork()创建子进程后,共享了父进程的代码和数据,所以子进程的缓冲区里也还保留着两行数据,于是,加上父进程缓冲区中的数据,共有4行数据,fflush刷新缓冲区后,这4行数据被写到了log.txt中

不使用close,数据写到显示器上,因为显示器是行缓冲的,在调用printf和fprintf时,字符串中带有\n,就刷新缓冲区了,数据就被输出到了显示器上,write也将数据写到了显示器上。

关闭显示器时,write没有像printf和fprintf一样,写了两份数据到log.txt上,而wirte是系统调用的函数,printf和fprintf都是C语言提供的函数,所以这里的缓冲区是C语言提供的,不是系统提供的。因此,write写的数据不会放到该缓冲区里。

我们常说的缓冲区就是C语言提供的缓冲区(用户级),fflush就是将用户级的缓冲区往系统刷新。

使用 dup2 系统调用

使用close来关闭显示器的方法完成重定向太麻烦了,并且如果我们已经打开了文件,想完成重定向怎么办呢?dup/dup2就可以帮助我们实现。

dup太简单,我们直接用dup2

函数原型如下

#include <unistd.h>
int dup2(int oldfd, int newfd);

该函数的作用是将oldfd指向的位置的文件内容拷贝到newfd指向的位置,也就是将newfd指向的位置的文件覆盖,

示例代码

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
int main() 
{
    int fd = open("log.txt", O_CREAT | O_RDWR, 0644);
    if (fd < 0) 
    {
    	perror("open");
    	return 1;
    }
    
    //将文件log.txt替换显示器文件
    dup2(fd, 1);
    for (;;) 
    {
    	char buf[1024] = {0};
    	ssize_t read_size = read(0, buf, sizeof(buf) - 1);//向buf中输入内容
    	
        if (read_size < 0) 
        {
            perror("read");
            break;
        }
        
        printf("%s", buf);//将buf向stdout输出,实际上是对log.txt输出
        fflush(stdout);//刷新文件缓冲区,将内容写入
    }
    
    return 0;
}

效果展示:
image-20211004233615581

如果我们把dup2的新内容换成网卡,就是向网络写数据,也就是通信。

补充的小知识

files_struct里面有一个变量next_fd,它是指向下一个文件打开时,应该放的位置

如果当前0,1,2,3,4都被使用了,根据文件描述符的分配规则,next_fd就为5。

而如果我们将1关闭了,也就是0,2,3,4被占用了,那么next_fd就是1。

在minishell中添加重定向功能:

自己的实现:

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

#define NUM 256


int redir(char*cmd) 
{
  char*ptr = cmd;
  int redir_count = 0;
  int fd = -1;
  while (*ptr)
  {
    if (*ptr == '>')//找到'>'
    {
      *ptr++ = '\0';//为了将前面的命令独立,将该位置设置为'\0'
      ++redir_count;
    

        if (*ptr == '>')//第二个'>',也就是追加重定向
        {
          *ptr++ = '\0';
          ++redir_count;

        }

            //如果还有'>',则说明是错误的输入,返回1表示输入错误
        if (*ptr == '>')
        {
          printf("can not find ">>>\n");
          return 1;
        }

        //跳过空格,找到文件名的开头
        while (*ptr && isspace(*ptr))
        {
            ptr++;
        }

        char*file = ptr;//file就是文件名的开头

        //找到文件名的末尾,在文件名末尾添加'\0'
        while (*ptr && !isspace(*ptr))
        {
          ++ptr;
        }

        *ptr = '\0';


          //判断'>'的个数
        if (redir_count == 1)
        {
          fd = open(file, O_CREAT|O_RDWR|O_TRUNC, 0664);
        }
        else
        {
          fd = open(file, O_CREAT|O_RDWR|O_APPEND, 0664);
        }

        dup2(fd, 1);//将1号文件改成fd指向的文件
        close(fd); //去除fd,减少一个文件描述符的使用

    }// 第一个if的右括号
    ptr++;
      
  }//第一个while的右括号  
 
  return 0;
}


int main()
{
  //制造命令行
  char *cmdline = "myshell@VM-4-11-centos study_10-4]# " ;
  //接收输入信息
  char cmd[NUM];

  //因为需要一直输入,所以用死循环
  while (1)
  {
    cmd[0] = '\0';
    printf("%s", cmdline);
    fgets(cmd, NUM, stdin);
    
    //将cmd的最后一个字符设置成\0,否则就是\n,程序替换时会把\n也当成命令
    cmd[strlen(cmd) - 1] = '\0';
    

  pid_t id = fork();

  if (id < 0)
  {
    perror("fork error!");
    exit(1);
  }

  if (id == 0)//子进程执行替换
  {
      //要让子进程执行
    int j = redir(cmd);
    if (j != 0)//说明不是正常结束,有多个'>'
      exit(1);

    //用指针数组接收分割的字符串
    char*args[NUM];
    args[0] = strtok(cmd, " ");
    int i = 1;
    do
    {
      args[i] = strtok(NULL, " ");
      
      //当strtok返回NULL时,说明分割结束
      //这个NULL刚好作为替换函数中数组参数的最后的那个NULL
      if (args[i] == NULL)
      {
        break;
      }
      ++i;
    } while (1);
  
    //因为我们实现的都是shell命令,可在PATH中找到,所以使用带p的替换函数
    //第一个参数就是args数组的第一个元素
    
    execvp(args[0], args);
   
    //如果执行到这里,说明替换失败
    exit(1);
  }
  else//父进程等待子进程执行 
  {
    pid_t st = 0;
    //阻塞式等待
    pid_t ret = waitpid(id, &st, 0);
    

    if (ret > 0 && ((st>>8) & 0xff) == 0)//正常运行,结果正确
    {
        ;
    }
    else if (ret > 0 && ((st>>8) & 0xff) != 0)//正常运行,结果错误
    {
      printf("Excution error!\n");
      printf("exit code:%d\n", (st>>8) & 0xff);
    }
    else //错误退出
    {
      printf("running error!\n");
    }

  }//判断父子进程的else的右括号

  }
  return 0;
}

printf是C库当中的IO函数,一般往 stdout 中输出,但是stdout底层访问文件的时候,找的还是fd:1, 但此时,fd:1
下标所表示内容,已经变成了myfile的地址,不再是显示器文件的地址,所以,输出的任何消息都会往文件中写
入,进而完成输出重定向。

评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

WoLannnnn

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值