C | Unix编程 | 2 - 文件描述符

517 阅读9分钟

前言

主要描述Unix文件系统设计,以及如何使用它们。

文件描述符

对于内核而言,几乎所有I/O都是在对文件描述符fd(也称为文件句柄)的操作。是一个小的非负整数。 使用文件描述符的好处是:系统调用的时候,不需要知道它是一个什么具体类型(文件,管道pipe,套接字socket)。

几乎所有stdin,stdout,stderr,都被声明为0、1、2 但是我们不应在代码中使用具体数字,而应该使用对应的符号定义。

下面图展示了,使用文件描述符的好处,允许字节流从一个流向另外一个,但每个程序保留向屏幕生成错误消息的能力

image.png 将两个程序的标准输入输出进行连接 image.png

fd固然好处多,但是一个进程能打开的最大fd数量有限制吗?研究一下

最大值测试

getconf(1) 查询

image.png

getrlimit(2)查询

image.png

sysconf(3)查询

image.png

image.png

编写一个测试单进程可打开文件数最大值的程序,程序主要功能步骤描述为:

    ->先使用getconf查询系统配置
    ->再使用sysconf获取运行时的配置
    ->动态改变fd数量软限制值
    ->再使用getrlimit获取fd资源限制数量num
    ->再计算当前已打开fd数量
    ->再在在/dev/null通道上打开额外的fd,数量级为num个
    
    ->验证时候配合系统ulimit -n命令查看程序行为变化

实现程序

#include "apue.h"

#include <sys/time.h>
#include <sys/resource.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>

/**
 * 计算当前打开的fd数量
 * @param num
 * @return
 */
int countOpenFiles(int num) {
  struct stat stats;
  int count = 0;
  for (int i = 0; i < num; i++) {
   if (fstat(i, &stats) == 0) { //获取文件状态信息
     printf("Currently open: fd #%d (inode %ld)\n", i,
            stats.st_ino);
     count++;
   }
  }

  return count;
}

/**
 * 软设置当前进程能打开的FD数量最大值
 * @param softmax
 */
void setSoftLimitFdMax(int softmax){

  struct rlimit rlim;

  rlim.rlim_cur = softmax; //动态改变软限制值
  rlim.rlim_max = 1024;//硬设置最大值,参照前面的输出

  if (setrlimit(RLIMIT_NOFILE, &rlim) == -1) {
   perror("setrlimit");
   exit(EXIT_FAILURE);
  }

  printf("The new soft limit for the number of files is: %lld\n",
         (long long)rlim.rlim_cur);
}


void openFiles(int num) {
  int count, fd;

  count = countOpenFiles(num);

  printf("Currently open files: %d\n", count);

  for (int i = count; i <= num ; i++) {
   if ((fd = open("/dev/null", O_RDONLY)) < 0) {//对 虚空 /dev/null 继续打开
     if (errno == EMFILE) {
      printf("Opened %d additional files, then failed: %s (%d)\n", i - count, strerror(errno), errno);
      break;
     } else {
      fprintf(stderr, "Unable to open '/dev/null' on fd#%d: %s (errno %d)\n",
              i, strerror(errno), errno);
      break;
     }
   }
  }
}
int main(int argc,char *argv[]){
  int openmax=NULL;
#ifdef OPEN_MAX
  printf("OPEN_MAX is defined as %d.\n", OPEN_MAX);
#else
  printf("OPEN_MAX is not defined on this platform.\n"); //OPEN_MAX未被定义在此平台
#endif


  printf("'getconf OPEN_MAX' says: ");
  (void)fflush(stdout);
  (void)system("getconf OPEN_MAX"); // 打印getconf的值

  errno = 0;
  if((openmax= sysconf(_SC_OPEN_MAX))<0){ // 不合理场景
    if (errno == 0) {
     fprintf(stderr, "sysconf(3) considers _SC_OPEN_MAX unsupported?\n");
    } else {
     fprintf(stderr, "sysconf(3) error for _SC_OPEN_MAX: %s\n",
             strerror(errno));
    }
    exit(EXIT_FAILURE);
  }
  printf("sysconf(3) says this process can open %ld files.\n", openmax);// 打印sysconf设置的值

  setSoftLimitFdMax(500); // 软设置

  struct rlimit rlp;
  if (getrlimit(RLIMIT_NOFILE, &rlp) != 0) { // Historically, this limit was named RLIMIT_OFILE on BSD
   fprintf(stderr, "Unable to get per process rlimit: %s\n", strerror(errno));
   exit(EXIT_FAILURE);
  }
  openmax = (int)rlp.rlim_cur;
  printf("getrlimit(2) says this process can open %d files.\n", openmax);

  openFiles(openmax);// 测试可打开文件数
  return 0;
}

测试输出

image.png 可以看到:

step1:我们将软设置最大文件打开数量500后的输出内容。
step2:然后通过系统命令ulimit将原来的1024限制到200后,sysconf(3)和getconf(1)的可打开数量限制到了200,然后在程序中通过软设置又设置到了500(注意此时硬设置的值仍然为1024)。

说明进程文件打开数量是可配置的。

试试ulimit -n ulimit的数量值

image.png

文件函数

open

       #include <fcntl.h>         
       int open(const char *pathname, int flags);                                                                                                      
       int open(const char *pathname, int flags, mode_t mode);   
      
       int openat(int dirfd, const char *pathname, int flags);                                                                                         
       int openat(int dirfd, const char *pathname, int flags, mode_t mode); 
       //返回值: file descriptor if OK, -1 on error

open函数flag值

下面几种flag值必须指定(且只能)指定其中一个:

O_RDONLY - open for reading only 
O_WRONLY - open for writing only 
O_RDWR - open for reading and writing

下面flag可以使用按位或(|)运算符进行组合使用:

  O_APPEND:每次写操作都是追加到文件末尾,而不是覆盖已有数据。
  O_CREAT:如果文件不存在,则创建一个新文件。如果已经存在,则打开该文件。
  O_EXCL:与O_CREAT一同使用,如果文件不存在,则创建,否则报错。
  O_TRUNC:将文件截断为0长度,即清空文件内容。
  O_NONBLOCK:开启非阻塞模式,打开文件时不阻塞程序执行,且读写时不等待数据就绪。
  O_SYNC:强制将数据写入物理设备后再返回,即等待物理I/O完成。

下面是一些可能会在某些操作系统平台上支持的open函数oflags:

  O_DIRECTORY:如果路径解析为非目录文件,则失败并将errno设置为ENOTDIR。
  O_DSYNC:等待数据进行物理I/O操作,但不包括文件属性。
  O_EXEC:仅供执行使用的文件打开方式,如果是目录则失败。
  O_NOFOLLOW:不跟随符号链接打开文件。
  O_PATH:仅获取用于fd级别操作的文件描述符。(仅适用于Linux >2.6.36)
  O_RSYNC:阻塞读操作,直到所有挂起的写完成。
  O_SEARCH:仅用于搜索的打开方式,如果是普通文件则失败。

例如:

int fd = open("file.txt", O_RDWR | O_CREAT | O_TRUNC, S_IRWXU);

以上代码表示以读写方式打开文件“file.txt”,如果文件不存在则创建它,如果已经存在则将其截断为0,并设置文件权限为0666

open函数mode值

可以使用man 2 open找到以下描述

              The following symbolic constants are provided for mode:                                                                                  
                                                                                                                                                       
              S_IRWXU  00700 user (file owner) has read, write, and execute permission                                                                 
                                                                                                                                                       
              S_IRUSR  00400 user has read permission                                                                                                  
                                                                                                                                                       
              S_IWUSR  00200 user has write permission                                                                                                 
                                                                                                                                                       
              S_IXUSR  00100 user has execute permission                                                                                               
                                                                                                                                                       
              S_IRWXG  00070 group has read, write, and execute permission                                                                             
                                                                                                                                                       
              S_IRGRP  00040 group has read permission                                                                                                 
                                                                                                                                                       
              S_IWGRP  00020 group has write permission                                                                                                
                                                                                                                                                       
              S_IXGRP  00010 group has execute permission                                                                                              
                                                                                                                                                       
              S_IRWXO  00007 others have read, write, and execute permission                                                                           
                                                                                                                                                       
              S_IROTH  00004 others have read permission                                                                                               
                                                                                                                                                       
              S_IWOTH  00002 others have write permission                                                                                              
                                                                                                                                                       
              S_IXOTH  00001 others have execute permission                                                                                            
                                                                                                                                                       
              According to POSIX, the effect when other bits are set in mode is unspecified.  On Linux, the following  bits  are  also  honored  in    
              mode:                                                                                                                                    
                                                                                                                                                       
              S_ISUID  0004000 set-user-ID bit                                                                                                         
                                                                                                                                                       
              S_ISGID  0002000 set-group-ID bit (see inode(7)).                                                                                        
                                                                                                                                                       
              S_ISVTX  0001000 sticky bit (see inode(7)).      

比如:0666是由 S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH 组合而成的

open(2)的失败

open函数可能会因为某些原因失败:

EEXIST:指定了 O_CREAT | O_EXCL 选项,但文件已经存在;
EMFILE:进程已经达到了最大允许打开文件描述符数量;
ENOENT:文件不存在;
EPERM:权限不足。
... : 等等其他

一般使用规则为:

if ((fd = open(path, O_RDWR) < 0) {
    /* 错误处理 */ 
} 
/* 使用fd操作 */

close(2)

       #include <unistd.h>                                                                                                                             
                                                                                                                                                       
       int close(int fd);    
       //返回值: 0 OK, -1 on error

注意事项:

  • 关闭文件描述符会释放该文件上的任何记录锁(record locks);
  • 如果文件描述符没有被显式关闭,在进程终止时内核会自动关闭它们;
  • 为了避免泄露文件描述符,应始终在同一作用域内调用 close(2) 函数关闭它们。

read(2)

       #include  <unistd.h>                                                                                                                 
       ssize_t read(int fd, void *buf, size_t count);  
       //返回值:读取的字节数number ; 0 on EOF; -1 on error

Read从当前偏移量开始读取,并根据实际读取的字节数增加偏移量

在某些情况下,read返回的字节数可能少于请求的字节数count。例如:

  • 在读取请求的字节数之前达到EOF
  • 从网络中读取数据时,缓冲会导致数据到达的延迟
  • 面向记录的设备(磁带)可以一次返回一个记录的数据
  • 信号中断

write(2)

       #include <unistd.h>                                                                                                   
       ssize_t write(int fd, const void *buf, size_t count);
       //返回值:写入的字节数 if OK; -1 on error
  • write返回写入的字节数
  • 对于普通文件,写入从当前偏移量开始(除非指定了O_APPEND,在这种情况下,偏移量首先设置为文件的末尾)。
  • 写入后,偏移量根据实际写入的字节数进行调整

lseek(2)

       #include <sys/types>
       #include <unistd.h>
       
       off_t lseek(int fd, off_t offset, int whence);

whence 参数不同取值对 offset 参数所代表的含义的影响:

  • 若 whence 取值为 SEEK_SET,则表示相对于文件开头的偏移量,此时 offset 表示距离文件开头的字节数。
  • 若 whence 取值为 SEEK_CUR,则表示相对于当前文件位置的偏移量,此时 offset 表示相对于当前位置的字节数。
  • 若 whence 取值为 SEEK_END,则表示相对于文件结尾的偏移量,此时 offset 表示距离文件结尾的字节数。

lseek的灵活性,导致可能出现一些看似“奇怪”的操作

  • seek到一个负的位置,这在某些情况下可能是有意义的,例如从文件末尾向前查找某些信息。
  • seek 0从当前位置
  • seek 到文件结尾之后的位置,这样就可以向文件中追加数据

程序

#include "apue.h"
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>

#ifndef SLEEP
#define SLEEP 3
#endif

const char *file_path = "./newfile.txt";

void createFailedFile() {
  int fd;
  printf("check '%s' exists...\n", file_path);
  system("ls -l ./newfile.txt");
  printf("Try to create '%s' with O_RDWR ...\n", file_path);
  if ((fd = open("./newfile.txt", O_RDWR)) == -1) {
   perror("unable to create './newfile.txt' :");
   //exit(EXIT_FAILURE);
   return;
  }

  printf("'%s' created. File descriptor is: %d\n", file_path, fd);
  close(fd);

}

void createSuccessFile() {
  int fd;
  printf("check '%s' exists...\n", file_path);
  system("ls -l ./newfile.txt");
  printf("Try to create '%s' with O_RDWR | O_CREAT | O_TRUNC ...\n", file_path);
  if ((fd = open(file_path, O_RDWR | O_CREAT | O_TRUNC, S_IRWXU)) == -1) {
   perror("unable to create './newfile.txt' :");
   //exit(EXIT_FAILURE);
   return;
  }

  printf("'%s' created. File descriptor is: %d\n", file_path, fd);
  close(fd);

}

void writeFile() {
  int fd;
  if ((fd = open(file_path, O_RDWR)) == -1) {
   perror("unable to O_RDWR './newfile.txt' :");
   //exit(EXIT_FAILURE);
   return;
  }

  int wn, total_written = 0;
  size_t n = 4;
  const char *content = "abcdefgABCDEFG";
  while (total_written < strlen(content)) {
   wn = write(fd, content + total_written,
              strlen(content) - total_written);
   if (wn == -1) {
     // 写入出错,进行错误处理
     perror("write err");
     break;
   }
   total_written += wn;
  }
  close(fd);

}

void readFile() {
  int fd, n;
  if ((fd = open(file_path, O_RDONLY)) == -1) {
   perror("unable to O_RDWR './newfile.txt' :");
   return;
  }
  char bytes[1024];

  while ((n = read(fd, bytes, 1024)) > 0) {
   // 将数据写入标准输出
   write(STDOUT_FILENO, bytes, n);
  }
  close(fd);
}

void seekFile() {
  int fd;
  if ((fd = open(file_path, O_RDWR | O_CREAT | O_TRUNC, S_IRWXU)) == -1) {
   perror("unable to create './newfile.txt' :");
   //exit(EXIT_FAILURE);
   return;
  }
  __off_t offset = 0;
  int seek;
  if ((seek = lseek(fd, offset, SEEK_CUR)) == -1) {
   perror("seek err: ");
  } else {
   fprintf(stdout, "lseek SEEK_CUR offset=%ld : seek=%d", offset, seek);
  }

}

void keepForSecond() {
  system("ls -ls newfile.txt");
  printf("\n");
  sleep(SLEEP);
}

int main(int argc, char *argv[]) {
  createFailedFile();
  keepForSecond();

  createSuccessFile();
  keepForSecond();

  writeFile();
  system("cat newfile.txt");
  printf("\n");
  keepForSecond();

  readFile();
  keepForSecond();


  // seekFile();
  // keepForSecond();

  exit(EXIT_SUCCESS);
}

文件共享

由于Unix是一个多用户多任务系统。所以会出现多个进程同时操作一个文件的情况。

  • 每个进程表项都有一个文件描述符表,其中包含:
    • 文件描述符标志(例如FD_CLOEXEC,参见fcntl(2))
    • 指向文件表项的指针
  • 内核维护一个文件表;每个条目包含:
    • 文件状态标志(O_APPEND, O_SYNC, O_RDONLY等)。
    • 指向vnode表项的指针
    • 当前偏移量
  • 一个vnode结构,包含:
    • vnode信息
    • inode信息(比如当前文件大小)

示意图

image.png 如果同一进程打开同一文件两次,文件表不同,但v-node

image.png

如果不同进程打开同一文件:

image.png

fd控制

fcntl(2)

fcntl() 是一个用于操作文件描述符(file descriptor)的函数,它可以执行一系列操作,如改变文件状态标志、管理文件锁、获取或设置文件描述符标识等

       #include  <unistd.h>                                                                                                               
       #include  <fcntl.h>                                                                                                                             
                                                                                                                                                       
       int fcntl(int fd, int cmd, ... /* arg */ ); 

一些操作

  • F_DUPFD -重复的文件描述符
  • F_GETFD -获取文件描述符标志
  • F_SETFD -设置文件描述符标志
  • F_GETFL -获取文件状态标志
  • F_SETFL -设置文件状态标志
  • ...等等

ioctl(2)

ioctl() 是一个系统调用,可以操作特殊文件的底层设备参数。实现对设备的控制和传输数据,包括读取设备信息、设置设备状态、发送命令以及读写数据等。

       #include <sys/ioctl.h>                                                                                                                      
                                                                                                                                                       
       int ioctl(int fd, unsigned long request, ...); 

其中,fd 表示需要操作的设备文件的文件描述符;request 表示要执行的操作命令,通常是一个预定义的常量,其含义由具体的设备决定;第三个参数可以是任意类型,取决于不同的命令,可能需要传递数据的指针或结构体等。

fd复制

dup(2)

dup(2)是一个系统调用,用于创建文件描述符oldfd的副本,使用编号最低的未使用文件描述符作为新的descriptor。 成功返回后,旧的和新的文件描述符可以互换使用。它们引用相同的打开文件描述(参见open(2)),从而共享文件偏移量和文件状态标志;例如,如果使用lseek(2)对文件偏移量进行修改,其中一个文件描述符的偏移量也会为另一个文件描述符改变。 两个文件描述符不共享文件描述符标志(关闭)

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

总结

本文主要讲了类Unix系统下的文件描述符。及一些相关函数的使用。