dup,dup2函数用途
这两个函数主要用于文件描述符的复制(分配新文件描述符,dup和dup2)和重定向(对已经分配的文件描述符进行重定向,让其指向新的文件,dup2),复制得到的文件描述符和被复制的文件描述符共享文件表项,因此具有相同的文件偏移量和状态标志。
主要用途详细介绍:
- 重定向标准输入/输出:这是最常见的用法,特别是在子进程中,将标准输入、输出或错误输出重定向到其他文件描述符,这样标准输出就可以写在其他文件描述符对应文件了。
- 文件描述符的复制:在不同函数或线程中共享同一个文件描述符,以便不同的代码部分可以对同一个文件进行操作。
我们可能就会想,这和用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
函数是通文件描述符来实现文件的一些属性上的设置的函数,提供了多种操作,包括:
- 复制文件描述符:类似于
dup
和dup2
。 - 获取和设置文件描述符标志:例如设置文件描述符为非阻塞模式。
- 获取和设置文件状态标志:例如文件是否为只读、读写等。
- 获取和设置文件锁:用于文件加锁和解锁操作,提供文件级别的读写同步机制。
这个函数是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 操作(如 read
和 write
)一般是立即完成的,没有延迟。
阻塞模式和非阻塞模式
文件默认情况都是以阻塞模式打开的
阻塞模式
- 定义:在阻塞模式下,I/O 操作会等待数据准备好或操作完成后再返回。
- 应用:适用于普通文件、大多数常规文件操作。
- 行为:读取操作会等待直到数据被读取,写入操作会等待直到数据被写入。
非阻塞模式
- 定义:在非阻塞模式下,I/O 操作如果不能立即完成,会返回一个错误(例如,
EAGAIN
或EWOULDBLOCK
),而不是阻塞进程。 - 应用:适用于需要处理可能的 I/O 操作延迟或未准备好数据的场景,如管道、套接字、终端设备等。
- 行为:如果数据不能立即读取或写入,操作会立即返回,程序可以继续执行其他任务。
当进程执行阻塞操作时,它会进入阻塞状态,不能继续执行这个进程后续的代码,注意并不是让整个计算机都卡在了这个进程(听起来也不现实)。这个进程会等待阻塞停止的条件满足,例如数据准备好,之后才能恢复执行,继续执行这个进程后面的代码。
进程执行阻塞操作的详细过程
-
进程进入阻塞状态:
- 当进程执行一个阻塞操作(如等待 I/O 操作完成、等待锁或信号)时,如果该操作无法立即完成,进程会被操作系统挂起,进入阻塞状态。
- 在这种状态下,进程不再占用 CPU 资源,操作系统会将其从 CPU 的调度队列中移除。
-
上下文切换:
- 操作系统会进行上下文切换,将 CPU 资源分配给其他处于就绪状态的进程。
- 上下文切换涉及保存当前进程的状态(如寄存器、程序计数器)并加载另一个进程的状态,以便它可以继续执行。
-
等待条件满足:
- 被阻塞的进程会等待特定的事件或条件满足。例如,它可能在等待从磁盘读取数据,等待网络响应,或者等待某个锁被释放。
- 在等待期间,进程不会进行任何计算操作,也不会占用 CPU 资源。
-
进程唤醒:
- 一旦阻塞条件得到满足(例如,数据准备好、事件发生),操作系统会将进程从阻塞状态转为就绪状态。
- 进程会被放回到就绪队列,等待再次获得 CPU 资源以继续执行。
-
继续执行:
- 当 CPU 资源再次分配给该进程时,它会恢复执行,继续从它被阻塞时的位置接着执行代码。
文件为非阻塞模式打开进程对其处理方式
- 定义:在非阻塞模式下,I/O 操作如果不能立即完成,会返回一个错误(例如,
EAGAIN
或EWOULDBLOCK
),而不是阻塞进程。 - 应用:适用于需要处理可能的 I/O 操作延迟或未准备好数据的场景,如管道、套接字、终端设备等。
- 行为:如果数据不能立即读取或写入,操作会立即返回,程序可以继续执行后续代码。
普通文件和管道/套接字分别采用阻塞模式和非阻塞模式下的对比
-
普通文件:
- 阻塞模式:
read
调用会读取数据并返回。 - 非阻塞模式:
read
调用也会读取数据并返回,因为普通文件数据已经存在于磁盘上,I/O 操作只需将数据从磁盘读取到内存中,几乎是立即返回,不会返回EAGAIN
或EWOULDBLOCK
错误。
- 阻塞模式:
普通文件设置成非阻塞模式几乎没有作用,一般就阻塞模式就行了。
-
管道/套接字:
- 阻塞模式:
read
调用会阻塞直到有数据可读。 - 非阻塞模式:由于I/O 操作延迟或未准备好数据,
read
调用如果没有数据可,会立即返回EAGAIN
或EWOULDBLOCK
错误。
- 阻塞模式:
文件默认情况都是以阻塞模式打开的,如果想要更改为非阻塞模式,就可以用fcntl函数和命令字F_GETFL,将文件打开模式设置为O_NONBLOCK。普通文件一般不会设置为非阻塞模式,因为普通文件的I/O操作速度和数据获取速度都很快。管道和套接字有时会设置为非阻塞模式,以此来提高程序响应性。
注意使用fcntl函数和命令字F_GETFL来设置文件状态标志的时候不能对所有的文件状态标志进行设置,只要O_APPEND(追加模式), O_NONBLOCK(非阻塞模式),O_ASYNC(异步 I/O 操作)等几种可以,不能设置O_RDONLY等这些文件状态标志,不能设置的模式在上面的表格上有出现。