前言
主要描述Unix文件系统设计,以及如何使用它们。
文件描述符
对于内核而言,几乎所有I/O都是在对文件描述符fd(也称为文件句柄)的操作。是一个小的非负整数。 使用文件描述符的好处是:系统调用的时候,不需要知道它是一个什么具体类型(文件,管道pipe,套接字socket)。
几乎所有stdin,stdout,stderr,都被声明为0、1、2 但是我们不应在代码中使用具体数字,而应该使用对应的符号定义。
下面图展示了,使用文件描述符的好处,允许字节流从一个流向另外一个,但每个程序保留向屏幕生成错误消息的能力
将两个程序的标准输入输出进行连接
fd固然好处多,但是一个进程能打开的最大fd数量有限制吗?研究一下
最大值测试
getconf(1) 查询
getrlimit(2)查询
sysconf(3)查询
编写一个测试单进程可打开文件数最大值的程序,程序主要功能步骤描述为:
->先使用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;
}
测试输出
可以看到:
step1:我们将软设置最大文件打开数量500后的输出内容。
step2:然后通过系统命令ulimit将原来的1024限制到200后,sysconf(3)和getconf(1)的可打开数量限制到了200,然后在程序中通过软设置又设置到了500(注意此时硬设置的值仍然为1024)。
说明进程文件打开数量是可配置的。
试试ulimit -n ulimit的数量值
文件函数
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信息(比如当前文件大小)
示意图
如果同一进程打开同一文件两次,文件表不同,但v-node
如果不同进程打开同一文件:
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系统下的文件描述符。及一些相关函数的使用。