dup,dup2复制文件描述符,ioctl函数介绍,fcntl函数详解,阻塞模式和非阻塞模式介绍

dup,dup2函数用途

这两个函数主要用于文件描述符的复制(分配新文件描述符,dup和dup2)和重定向(对已经分配的文件描述符进行重定向,让其指向新的文件,dup2),复制得到的文件描述符和被复制的文件描述符共享文件表项,因此具有相同的文件偏移量和状态标志。

主要用途详细介绍:

  1. 重定向标准输入/输出:这是最常见的用法,特别是在子进程中,将标准输入、输出或错误输出重定向到其他文件描述符,这样标准输出就可以写在其他文件描述符对应文件了。
  2. 文件描述符的复制:在不同函数或线程中共享同一个文件描述符,以便不同的代码部分可以对同一个文件进行操作。

我们可能就会想,这和用open以相同方式打开同一个文件多次,得到的文件描述符有什么区别?

  • dup/dup2:用于复制文件描述符,新的文件描述符共享同一个文件表项,因此共享文件偏移量和状态标志。主要用于重定向标准输入/输出,或者在不同函数或线程中共享同一个文件描述符。对一个文件描述符的操作会影响其他指向同一文件的文件描述符。
  • open:用于打开文件,每次调用 open 都会创建一个新的文件表项,返回一个新的文件描述符。不同的文件描述符有独立的文件偏移量和状态标志。主要用于文件操作和创建新文件。对一个文件描述符的操作不会影响其他指向同一文件的文件描述符。

复制文件描述符的本质是使新文件描述符指向相同的文件表项,从而指向同一个打开的文件。这意味着新文件描述符和原文件描述符共享相同的文件状态(例如文件偏移量和文件状态标志)。

dup,dup2函数详解

dup函数会让系统返回一个进程当前未用最小的新文件描述符,该描述符指向与原文件描述符相同的文件表项,指向相同的文件,因此它们共享文件的读写偏移量和文件的状态标签。

比如使用lseek()对新文件描述符修改文件偏移量,这个操作会同时影响旧文件描述符oldfd。再如,使用read()对新文件描述符读取文件部分内容,改变了文件的读写位置,可以直接使用旧文件描述符读取该文件后续内容。

dup2()dup()不同的地方在于:

1. dup2可以用newfd参数指定新文件描述符的具体数值,而不局限于进程当前未用描述符的最小值。

2. dup2中,新文件描述符可以是已使用过的文件描述符,以此来让这个已使用过的文件描述符来对应oldfd的文件表项,这样一来,达到了重定向文件流的作用。dup2 函数可以改变文件描述符对应的文件来实现重定向。

dup和dup2使用示例:


    // 打开文件 a.txt ,获得其文件描述符 fd1
    // 此处 fd1 就代表了这个文件及其配套的系统资源
    int fd1 = open("a.txt", O_RDWR);

    // 复制文件描述符 fd1,默认得到最小未用的文件描述符
    dup(fd1);

    // 复制文件描述符 fd1,并指派为 100
    dup2(fd1, 100);

dup2的重定向解释

int fd=open(a.txt",O_RDWR);

比如说dup2(fd,1)就是让文件描述符fd对应的文件表项,复制给文件描述符1.

因为文件表项中存储了与文件操作相关的关键状态信息,如文件位置指针、文件状态标志、引用计数等,这样就使得文件描述符1对应的文件表项是我们自定义的文件,文件描述符1原本对应的输出的文件是我们的终端屏幕,现在就变成了输出在我们打开的一个文件中。

通过重定向,你可以改变文件描述符 1 的目标,对应指定的文件,从而使得 printf 输出的数据发送到你指定的文件或设备。但即使文件描述符 1 的目标被改变,printf 的默认行为仍然是使用文件描述符 1,只不过重定向之后,它的目标被改变了。如下图所示:

dup2使用示例

调用一个带有输出的程序,在终端中启动这个程序,让输出内容不要显示在终端,而是通过dup2函数改变printf的默认输出方向,标准printf输出保存在一个指定文件中。

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>

int main(int argc, char **argv){
    char buffer[BLKSIZE];
    if(argc!=2){
        printf("你必须指定一个文件来保存程序的输出内容/n");
        exit(0);
    }

    int fd=open(argv[1],O_RDWR);
    if(fd==-1){
        printf("打开文件失败/n");
    }
    dup2(fd,1);//文件描述符fd的文件表项复制给,文件描述符1的文件表项
    printf("hello world,i want change out target\n");
    return 0;
}

ioctl函数介绍

函数原型:int ioctl(int fd, unsigned long request, ...);

  • fd: 设备或文件的文件描述符。
  • request: 控制请求的命令或操作代码。
  • ...: 根据 request 的不同,这个参数可以是一个指向特定数据的指针,或者是其他类型的参数。它的含义取决于具体的 request 命令。

ioctl函数可以处理各种不同类型的设备和进行对设备的不同操作,也可以获取设备的里面很多参数,它是一个很古老的接口了,现在ioctl里面提供了各种类型的功能,由于是对不同类型设备操作,所以它的参数是不固定的。不同设备和品牌的 ioctl 请求码和参数格式会有所不同。在编写使用 ioctl 的代码时,必须参考特定设备的文档和驱动程序源码,以确定正确的请求码和参数格式。 

故此在这里我们不作详细探究。

fcntl函数

fcntl函数是通文件描述符来实现文件的一些属性上的设置的函数,提供了多种操作,包括:

  1. 复制文件描述符:类似于 dupdup2
  2. 获取和设置文件描述符标志:例如设置文件描述符为非阻塞模式。
  3. 获取和设置文件状态标志:例如文件是否为只读、读写等。
  4. 获取和设置文件锁:用于文件加锁和解锁操作,提供文件级别的读写同步机制。

这个函数是POSIX标准的一部分,广泛用于Unix和类Unix系统的文件操作。

该函数的名字是 file control 的缩写,顾名思义,它可以用来“控制”文件,与 ioctl 类似,此处的 “控制” 含义广泛,具体内容由其第二个参数命令字来决定,fcntl 的接口规范如下:

  • fcntl 是个变参函数,前两个参数是固定的,后续的参数个数和类型取决于 cmd 的具体数值。
  • 第二个参数 cmd,称为命令字。
  • 命令字有很多,常用的如下:

F_DUPFD 

  • 复制文件描述符,类似于 dup
  • 需要一个整型参数 arg,表示新文件描述符的最小值

int new_fd = fcntl(old_fd, F_DUPFD, 0); // 复制 old_fd,返回新的文件描述符 new_fd

0:表示新文件描述符的最小值,0 表示返回的文件描述符应为最小可用的文件描述符,是一个常用的默认值。如果你希望新文件描述符的值大于 0,可以传递其他值。第三个参数不能省略

F_GETFD和F_SETFD

F_GETFD

  • 用途:获取文件描述符标志。
  • 返回值:文件描述符标志,通常是 FD_CLOEXEC

int flags = fcntl(fd, F_GETFD); // 获取文件描述符标志

F_SETFD

  • 用途:设置文件描述符标志。
  • 参数:需要一个整型参数 arg,表示要设置的标志,常用标志是 FD_CLOEXEC

fcntl(fd, F_SETFD, FD_CLOEXEC); // 设置文件描述符标志为 FD_CLOEXEC

辨析

文件描述符标志文件状态标志是完全不一样的

文件描述符标志是与文件描述符关联的,影响文件描述符的行为。文件状态标志是与文件相关,影响文件操作的行为。它们针对不同的方面进行控制,一个是文件描述符级别,另一个是文件级别。

文件描述符标志的值常常设置为FD_CLOEXEC:在调用 exec 系列函数时自动关闭文件描述符,这里关于文件描述符标志的相关知识之后在进程那块我们来探究。

文件状态标志是我们之前学过的open操作中的对文件的一些规定,影响文件操作的行为,例如 O_RDONLY:只读模式,O_APPEND:每次写入都追加到文件末尾,O_NONBLOCK:以非阻塞模式打开文件。关于文件状态标志的获取和更改我们使用的cmd命令字是F_GETFL和F_SETFL,下面即将讲到。

F_GETFL和F_SETFL

F_GETFL用来获取文件状态标志,对应的fcntl函数返回值就是文件状态标志。

F_SETFD用来设置文件状态标志,需要一个整型参数 arg,表示要设置的标志。

设置新的文件状态时,一般都要先通过F_GETFL来获取文件状态标志,然后再用F_SETFD来进行设置,为什么呢?原因如下:

错误示范:

int fd = open("a.txt",O_RDWR);

fcntl(fd, F_SETFL, O_NONBLOCK);

为什么我们这样来写是错误的呢?

OK,让我们来回顾一下我们在学习open函数时候设置文件状态标志

fd = open("a.txt", O_WRONLY|O_CREAT|O_EXCL, 0644);你会发现很多时候我们设置的文件状态标志都是不止一个宏,而是用位或连接了多个宏。如果直接设置文件状态标志的话,那原来的文件状态标志就完全消失了,只剩下了新设置的 O_NONBLOCK,显然错误

正确示范:

先获取原先的文件状态标志,在原先的文件状态标志基础上变更我们新的文件状态标志

,最后通过命令字F_SETFL设置,为了代码更加完备,这里我们也可以检测返回值输出错误码。

int fd = open("example.txt", O_RDONLY);
int flags = fcntl(fd, F_GETFL);
flags |= O_NONBLOCK;
if (fcntl(fd, F_SETFL, flags) == -1) {
    perror("fcntl F_SETFL");
    close(fd);
    return 1;
}

这里的  O_NONBLOCK是将文件的打开方式设置成了非阻塞方式进行打开,这里我们的a.txt是一个普通文件,阻塞和非阻塞模式的区别通常情况不会显现(我们这里纯属为了展示fcntl函数中命令字F_GETFL的用法所以才设置成阻塞模式),因为对普通文件的文件 I/O 操作(如 readwrite)一般是立即完成的,没有延迟。

阻塞模式和非阻塞模式

文件默认情况都是以阻塞模式打开的

阻塞模式
  • 定义:在阻塞模式下,I/O 操作会等待数据准备好或操作完成后再返回。
  • 应用:适用于普通文件、大多数常规文件操作。
  • 行为:读取操作会等待直到数据被读取,写入操作会等待直到数据被写入。
非阻塞模式
  • 定义:在非阻塞模式下,I/O 操作如果不能立即完成,会返回一个错误(例如,EAGAINEWOULDBLOCK),而不是阻塞进程。
  • 应用:适用于需要处理可能的 I/O 操作延迟未准备好数据的场景,如管道、套接字、终端设备等。
  • 行为:如果数据不能立即读取或写入,操作会立即返回,程序可以继续执行其他任务。

当进程执行阻塞操作时,它会进入阻塞状态,不能继续执行这个进程后续的代码,注意并不是让整个计算机都卡在了这个进程(听起来也不现实)。这个进程会等待阻塞停止的条件满足,例如数据准备好,之后才能恢复执行,继续执行这个进程后面的代码。

进程执行阻塞操作的详细过程

  • 进程进入阻塞状态

    • 当进程执行一个阻塞操作(如等待 I/O 操作完成、等待锁或信号)时,如果该操作无法立即完成,进程会被操作系统挂起,进入阻塞状态
    • 在这种状态下,进程不再占用 CPU 资源,操作系统会将其从 CPU 的调度队列中移除。
  • 上下文切换

    • 操作系统会进行上下文切换,将 CPU 资源分配给其他处于就绪状态的进程。
    • 上下文切换涉及保存当前进程的状态(如寄存器、程序计数器)并加载另一个进程的状态,以便它可以继续执行。
  • 等待条件满足

    • 被阻塞的进程会等待特定的事件或条件满足。例如,它可能在等待从磁盘读取数据,等待网络响应,或者等待某个锁被释放。
    • 在等待期间,进程不会进行任何计算操作,也不会占用 CPU 资源。
  • 进程唤醒

    • 一旦阻塞条件得到满足(例如,数据准备好、事件发生),操作系统会将进程从阻塞状态转为就绪状态。
    • 进程会被放回到就绪队列,等待再次获得 CPU 资源以继续执行。
  • 继续执行

    • 当 CPU 资源再次分配给该进程时,它会恢复执行,继续从它被阻塞时的位置接着执行代码

文件为非阻塞模式打开进程对其处理方式

  • 定义:在非阻塞模式下,I/O 操作如果不能立即完成,会返回一个错误(例如,EAGAINEWOULDBLOCK),而不是阻塞进程。
  • 应用:适用于需要处理可能的 I/O 操作延迟或未准备好数据的场景,如管道、套接字、终端设备等。
  • 行为:如果数据不能立即读取或写入,操作会立即返回,程序可以继续执行后续代码。

普通文件和管道/套接字分别采用阻塞模式和非阻塞模式下的对比

  • 普通文件

    • 阻塞模式read 调用会读取数据并返回。
    • 非阻塞模式read 调用也会读取数据并返回,因为普通文件数据已经存在于磁盘上,I/O 操作只需将数据从磁盘读取到内存中,几乎是立即返回,不会返回EAGAINEWOULDBLOCK 错误。

普通文件设置成非阻塞模式几乎没有作用,一般就阻塞模式就行了。

  • 管道/套接字

    • 阻塞模式read 调用会阻塞直到有数据可读。
    • 非阻塞模式:由于I/O 操作延迟或未准备好数据,read 调用如果没有数据可,会立即返回 EAGAINEWOULDBLOCK 错误。

文件默认情况都是以阻塞模式打开的,如果想要更改为非阻塞模式,就可以用fcntl函数和命令字F_GETFL,将文件打开模式设置为O_NONBLOCK。普通文件一般不会设置为非阻塞模式,因为普通文件的I/O操作速度和数据获取速度都很快。管道和套接字有时会设置为非阻塞模式,以此来提高程序响应性。

注意使用fcntl函数和命令字F_GETFL来设置文件状态标志的时候不能对所有的文件状态标志进行设置,只要O_APPEND(追加模式), O_NONBLOCK(非阻塞模式),O_ASYNC(异步 I/O 操作)等几种可以,不能设置O_RDONLY等这些文件状态标志,不能设置的模式在上面的表格上有出现。

  • 25
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值