【Linux】文件IO--fcntl/lseek/阻塞与非阻塞/文件偏移

fcntl函数

基本内容

fcntl函数用于改变文件的属性

参数:fd文件描述符、命令(例:F_GETFL、F_SETFL)

当使用F_GETFL时,后面不用额外多传任何参数

当使用F_SETFL时,后面需要传入修改的文件属性

下面给出一段代码:

cpp 复制代码
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<stdlib.h>
#include<fcntl.h>
int main(){
    int fd= open("./dict.txt",O_RDONLY);
    printf("./dict: fd=%d\n",fd);
    int flags = fcntl(fd,F_GETFL);
    flags |= O_TRUNC;
    fcntl(fd,F_GETFL,flags);
    return 0;
}

根据这个例子我们不能明显的根据现象来判断是否真的添加了权限。我们来说一下,阻塞与非阻塞的概念:

阻塞与非阻塞

在Linux编程中,阻塞与非阻塞 是指进程或线程在进行某些操作(如IO操作、网路通信等)时的不同行为。阻塞 :当一个进程或线程发起一个操作(如读取数据)时,如果没有满足条件的数据可用,它将挂起 (阻塞)直到数据可用或操作完成。它的特点是确保操作完成后才能执行后续代码,简单易用。非阻塞 :面对上述情况,它不会挂起 ,而是立即返回 控制权,并提供一个状态(例如,没有数据可用)。它的特点是适合需要同时处理多个任务的情况,可以通过轮询事件驱动方式处理IO,复杂度较高,需要处理可能多次尝试读取再失败的情况。

一般来讲,常规文件没有阻塞与非阻塞的概念,只有设备文件 (像/dev目录下的文件,我们后续以/dev/tty终端设备文件举例;)还有网络文件,被读取时,才会有阻塞的现象发生。

下面我们给出第一个示例代码:

cpp 复制代码
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>

#define BUFFER_SIZE 10
int main(){
    char buf[BUFFER_SIZE];
    int n = read(STDIN_FILENO,buf,BUFFER_SIZE);
    if(n < 0){            
        perror("read STDIN_FILENO");
        exit(1);
    }
    write(STDOUT_FILENO,buf,n);
    return 0;
}

当读取标准输入这个文件时,由于我们没有使用键盘等输入设备输入内容,那么read将无法读取任何内容,但是由于文件的属性是阻塞的,所以我们也执行不到下一句,此时我们从键盘输入一些内容(例如hello),那么紧跟着read就读到了内容:hello,然后就像标准输出也就是终端中,输出hello,然我们验证一下:

答案是正确的。这就是阻塞,下面第二个例子就是读取这个文件时设置为非阻塞状态的然后再读取:

我们知道,当我们打开这个终端时,标准输入和输出就已经被维护在了这个进程之中,也就是已经打开了这几个文件,所以上面我们没有写open函数去打开这两个文件,而是直接使用其文件描述符的宏。那么我们将其重新打开,并给其非阻塞的属性:

cpp 复制代码
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<fcntl.h>
#include<errno.h>

#define BUFFER_SIZE 10
int main()
{
    char buf[BUFFER_SIZE];
    int fd = open("/dev/tty",O_RDONLY|O_NONBLOCK);
    if(fd<0){
        perror("open /dev/tty error");
        exit(1);    
    }
    int n=0;
    while(1){
        n=read(fd,buf,BUFFER_SIZE);
        if(n>=0)break;
        if(errno!=EAGAIN){
            perror("read /dev/tty error");
            exit(1);                    
        }else{
            write(STDOUT_FILENO,"try again\n",strlen("try again\n"));
            sleep(5);                                
        }
    }
    write(STDOUT_FILENO,buf,n);           
    close(fd);
    return 0;
}

对于该程序,我们以非阻塞(O_NONBLOCK)的方式打开,如果我们读取时遇到阻塞的状态,read会立即返回-1,但是如何将读取失败和遇到阻塞做区分呢?答案是,遇到阻塞时会同时设置errno值为EAGAIN 或者是EWOULDBLOCK,遇到这种情况,说明read没有失败,而是而是read在以非阻塞方式读取一个设备文件(or网络文件),并且文件无数据。

那么,我们read完之后,对其返回值进行判断,如果返回值大于等于0,即读取成功了,那么就跳出while循环,执行正常的write,然后关闭文件。如果返回值小于零,就要对errno的值进行判断,若不等于EAGAIN或EWOULDBLOCK,那么read就是正常情况下的读取失败,而非阻塞。反之等于EAGAIN的值,那么我们就向屏幕中输出一句"try again\n",提示用户,程序将再一次尝试读取数据。然后睡眠5秒,继续下次循环,直至能够读取到数据。

下面给出运行结果:

每隔两秒,屏幕中将会显示一次尝试重新读取的提示。这就是非阻塞,以一种与用户一直保持互动的方式进行。但是我们就让它一直这么执行下去吗?显然不行,所以我们一般会设置一个时间限制,如果超出时间限制,我们将不再进行读取,对上面的文件进行改动:

cpp 复制代码
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<fcntl.h>
#include<errno.h>

#define BUFFER_SIZE 1024
#define TIME_LIMITS 10
#define TIME_GAPS 2
int main()
{
    char buf[BUFFER_SIZE];
    int fd = open("/dev/tty",O_RDONLY|O_NONBLOCK);
    if(fd<0){
        perror("open /dev/tty error");
        exit(1);    
    }
    int i=0, n=0, maxcnt=TIME_LIMITS / TIME_GAPS;
    while( i < maxcnt ){
        n=read(fd,buf,BUFFER_SIZE);
        if(n>=0)break;
        if(errno!=EAGAIN){
            perror("read /dev/tty error");
            exit(1);                    
        }else{
            write(STDOUT_FILENO,"try again\n",strlen("try again\n"));
            sleep(TIME_GAPS);
            ++i;                                
        }
    }
    if(i >= maxcnt) write(STDOUT_FILENO,"time out\n",strlen("time out\n"));
    else write(STDOUT_FILENO,buf,n);           
    close(fd);
    return 0;
}

此时再来看一下运行结果:

我们设置了时间限制为10秒,时间间隔为2秒,即轮询五次,如果这五次没有得到结果,那么我将作超时处理。

【注意:阻塞与非阻塞是文件的属性,不是read的行为】

示例程序-改变文件属性

对于上面的程序,我们都是重新打开open终端程序,与此同时赋予其非阻塞属性。而fcntl给出了不重新打开就能实现改变其阻塞属性的方案:

cpp 复制代码
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<fcntl.h>
#include<errno.h>

#define BUFFER_SIZE 1024
#define TIME_LIMITS 10
#define TIME_GAPS 2
int main()
{
    char buf[BUFFER_SIZE];
    int flags = fcntl(STDIN_FILENO,F_GETFL);//获取标准输入的flags
    flags |= O_NONBLOCK;//添加上非阻塞属性
    fcntl(STDIN_FILENO,F_SETFL,flags);//重新设置标准输入的flags 
    
    int i=0, n=0, maxcnt=TIME_LIMITS / TIME_GAPS;
    while( i < maxcnt ){
        n=read(STDIN_FILENO,buf,BUFFER_SIZE);
        if(n>=0)break;
        if(errno!=EAGAIN){
            perror("read /dev/tty error");
            exit(1);                    
        }else{
            write(STDOUT_FILENO,"try again\n",strlen("try again\n"));
            sleep(TIME_GAPS);
            ++i;                                
        }
    }
    if(i >= maxcnt) write(STDOUT_FILENO,"time out\n",strlen("time out\n"));
    else write(STDOUT_FILENO,buf,n);
    return 0;
}

达到了同样的效果。但对于下面这三行中的第二行,用到了位或,这里涉及到一个叫做"位图"的知识。

位图

位图在 Linux 中通常用来表示集定的状态或选项。它通过一组位来表示不同的状态,每一位对应一种标志。例如:权限位状态标志等。

在该图中,箭头所在的那几个位分别就表示了O_CREAT、O_TRUNC、O_NONBLOCK这三种权限位。

位或运算符 | 可以在位图中用来设置特定位。这能有效地启用某些功能或状态而不会更改其他位的值。

  • 设置标志:通过位或操作,可以将某个标志位设置为1,而保持其他位不变。
  • 组合标志:可以使用位或操作将多个标志合并成一个值。

lseek函数

基本内容

函数描述:移动文件指针

函数原型: off_t lseek(int fd, off_t offset, int whence);

头文件

● #include <sys/types.h>

● #include <unistd.h>

函数参数

● fd:文件描述符

● offset:字节数,以whence参数为基点解释offset

● whence:解释offset参数的基点

○ SEEK_SET:文件偏移量设置为offset

○ SEEK_CUR:文件偏移量设置为当前文件偏移量加上offset,offset可以为负数

○ SEEK_END:文件偏移量设置为文件长度加上offset,offset可以为负数

函数返回值:

● 若lseek成功执行,则返回新的偏移量。

● 失败返回-1并设置errno

文件偏移量

文件偏移量(current file offset) ,也叫读写偏移量和指针,通常为非负整数

每个打开的文件都记录着当前读写位置,打开文件时读写位置是0,表示文件开头,通常读写多少个字节,就会将读写位置往后移动多少个字节。但是有一个例外,如果以O_APPEND方式打开,每次写操作都会在文件末尾追加数据,然后将读写位置移动到新的文件末尾。lseek和标准IO库的fseek函数类似,可以移动当前读写位置(或者叫偏移量)。

【注意:读 和 写使用同一偏移位置】

常见用法

● 文件指针移动到头部

○ lseek(fd, 0, SEEK_SET);

● 获取文件指针当前位置

○ int len = lseek(fd, 0, SEEK_CUR);

● 获取文件长度

○ int len = lseek(fd, 0, SEEK_END);

● lseek实现文件拓展(其他函数:int truncate(const char *path,off_t length);)

lseek允许超过文件结尾设置偏移量,文件因此会被拓展。

○ lseek(fd, n, SEEK_END); // 扩展n个字节

○ write(); // 扩展后需要执行一次写操作才能扩展成功

cpp 复制代码
int fd = open("./a.txt", O_RDWR);
char buf[1024];
int read_count = read(fd, buf, sizeof(buf));

// 获取当前偏移量
int offset = lseek(fd, 0, SEEK_CUR);
printf("current file offset = %d\n", offset);
// 获取文件长度
int len = lseek(fd, 0, SEEK_END);
printf("current file length = %d\n", len);
// 文件指针指向头部
lseek(fd, 0, SEEK_SET);
//从文件尾部开始向后扩展1000个字节
lseek(fd, 1000, SEEK_END);
write(fd, " ", 1); // 为了扩展成功,随便写入一个数据
len = lseek(fd, 0, SEEK_END);
printf("current file length = %d\n", len);

^@:文件空洞


感谢大家!

相关推荐
lancyu3 分钟前
C语言--插入排序
c语言·算法·排序算法
Asthenia041223 分钟前
编译原理基础:LL(1) 文法与 LL(1) 分析法
后端
Asthenia041243 分钟前
编译原理基础:FIRST 集合与 FOLLOW 集合的构造与差异
后端
H1346948901 小时前
企业服务器备份软件,企业服务器备份的方法有哪些?
运维·服务器·负载均衡
Asthenia04121 小时前
编译原理基础:FOLLOW 集合与 LL(1) 文法条件
后端
skywalk81631 小时前
OpenRouter开源的AI大模型路由工具,统一API调用
服务器·前端·人工智能·openrouter
Asthenia04121 小时前
编译原理基础:FIRST 集合与提取公共左因子
后端
愚润求学1 小时前
Linux开发工具——apt
linux·服务器·开发语言
杰克逊的日记2 小时前
CentOs系统部署DNS服务
linux·python·centos·dns
欧宸雅2 小时前
Clojure语言的持续集成
开发语言·后端·golang