UNIX环境编程-系统调用IO(8)

一、文件描述符

视频教程

image-20230717195108379

文件描述符的实质:一个整型数字,数组下标

在 Unix 和类 Unix 系统(如 Linux)中,文件描述符(file descriptor)是一个抽象的概念,用于表征一个打开的文件或者其他输入/输出资源,如管道或网络套接字。

文件描述符通常是一个非负整数。当一个进程打开一个现有文件或者创建一个新文件时,内核会创建一个文件描述符来表征这个文件。然后,进程可以使用这个文件描述符来读取文件、写入文件,或者进行其他操作。

例如,在 C 语言中,你可以使用 open 系统调用来打开一个文件,然后得到一个文件描述符。然后你可以使用 read、write 或其他系统调用来操作这个文件

通常情况下,每个 Unix 或类 Unix 进程都会有三个预定义的文件描述符:标准输入(0)、标准输出(1)、和标准错误输出(2) 文件描述符的数组中,前三个值是系统给定的(0,1,2)。这些文件描述符通常分别关联到键盘输入和终端的输出。

1
write(1,"b",1)  // 使用系统IO,向标准输出中写入 一个字符b

文件描述符是 Unix 风格操作系统进行 I/O 操作的主要方式。




二、系统调用IO函数

1.open、close

(1)open

在Linux或Unix操作系统中,open是一个系统调用,用于打开文件并返回一个文件描述符。文件描述符是一个用于访问和管理打开文件的抽象指示器。你可以通过这个文件描述符进行读取,写入,或者关闭文件等操作。

以下是open函数的原型:

1
2
3
4
#include <fcntl.h>

int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);

open函数接收两个或三个参数:

  • pathname:文件的路径。
  • flags:打开文件的模式,比如 O_RDONLY(只读),O_WRONLY(只写),O_RDWR(读写),O_CREAT(如果文件不存在,则创建文件),O_TRUNC(如果文件已存在且为可写,则清空文件内容),等等。
  • mode:当创建新文件时(即使用了O_CREAT标志),mode参数用于指定新文件的权限。这是一个可选参数。

return : open函数返回一个非负的文件描述符,如果打开文件失败,则返回-1。

以下是一个简单的示例,该示例使用open函数打开一个文件进行写入操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <fcntl.h>
#include <unistd.h>

int main() {
int fd = open("test.txt", O_WRONLY | O_CREAT, 0644);
if (fd == -1) {
/* Handle error */
} else {
/* Use the file descriptor for writing */
close(fd);
}
return 0;
}

在这个示例中,如果文件test.txt不存在,那么open函数会创建这个文件,文件的权限会被设置为0644(这意味着文件的所有者可以读写文件,其他用户只能读取文件)。然后,你可以使用返回的文件描述符fd进行写入操作。完成写入操作后,别忘了使用close函数关闭文件。

在使用open函数时,务必检查其返回值以确保文件已成功打开。如果文件打开失败,open函数将返回-1,并设置全局变量errno以指示发生了什么错误。



2.read、write、lseek以及mycopy的实现

(1)read

在Linux或Unix系统中,read是一个系统调用,用于从打开的文件描述符中读取数据。以下是read函数的原型:

1
2
3
#include <unistd.h>

ssize_t read(int fd, void *buf, size_t count);

read函数接收三个参数:

  • fd:要读取的文件描述符。这通常是一个先前使用open函数打开的文件的文件描述符。
  • buf:一个缓冲区,用于存储从文件描述符中读取的数据。
  • count:要从文件描述符中读取的字节数。

read函数返回实际读取的字节数。如果发生错误,它将返回-1,并设置全局变量errno以指示发生了什么错误。

在Linux或Unix系统中,read是一个系统调用,用于从打开的文件描述符中读取数据。以下是read函数的原型:

1
2
3
cCopy code#include <unistd.h>

ssize_t read(int fd, void *buf, size_t count);

read函数接收三个参数:

  • fd:要读取的文件描述符。这通常是一个先前使用open函数打开的文件的文件描述符。
  • buf:一个缓冲区,用于存储从文件描述符中读取的数据。
  • count:要从文件描述符中读取的字节数。

read函数返回实际读取的字节数。如果发生错误,它将返回-1,并设置全局变量errno以指示发生了什么错误。

以下是一个简单的示例,该示例使用read函数从一个文件中读取数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <fcntl.h>
#include <unistd.h>

int main() {
char buffer[128];
int fd = open("test.txt", O_RDONLY);
if (fd == -1) {
/* Handle error */
} else {
ssize_t bytesRead = read(fd, buffer, sizeof(buffer) - 1);
if (bytesRead == -1) {
/* Handle error */
} else {
buffer[bytesRead] = '\0'; // Null-terminate the string
/* Now buffer contains the first sizeof(buffer) - 1 bytes of the file. */
}
close(fd);
}
return 0;
}

在这个示例中,我们打开一个名为test.txt的文件,然后尝试从该文件中读取最多127个字节的数据(我们预留一个字节用于字符串的空字符终止符)。然后,我们在读取的数据后添加一个空字符,使其成为一个有效的C字符串。如果读取过程中发生错误,我们需要处理该错误。

使用read函数时,务必检查其返回值,以确保没有发生错误并且已成功读取到数据。当你读到文件的末尾时,read函数将返回0,表示没有更多的数据可以读取。


(2)write

在Linux或Unix系统中,write是一个系统调用,用于将数据写入到打开的文件描述符。以下是write函数的原型:

1
2
3
#include <unistd.h>

ssize_t write(int fd, const void *buf, size_t count);

write函数接收三个参数:

  • fd:要写入的文件描述符。这通常是一个先前使用open函数打开的文件的文件描述符。
  • buf:包含要写入的数据的缓冲区。
  • count:要写入的字节数。

write函数返回实际写入的字节数。如果发生错误,它将返回-1,并设置全局变量errno以指示发生了什么错误。

以下是一个简单的示例,该示例使用write函数向一个文件中写入数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

int main() {
const char *text = "Hello, World!\n";
int fd = open("test.txt", O_WRONLY | O_CREAT, 0644);
if (fd == -1) {
/* Handle error */
} else {
ssize_t bytesWritten = write(fd, text, strlen(text));
if (bytesWritten == -1) {
/* Handle error */
} else if ((size_t)bytesWritten < strlen(text)) {
/*若此时写入的字节数小于text的长度则报错*/
/* Handle partial write */
}
close(fd);
}
return 0;
}

在这个示例中,我们打开(或创建)一个名为test.txt的文件,然后尝试向该文件中写入一个字符串。如果写入过程中发生错误,我们需要处理该错误。我们还检查了write函数是否已成功写入所有的数据。

使用write函数时,务必检查其返回值,以确保没有发生错误并且所有数据都已成功写入。在某些情况下(例如,磁盘空间不足),write函数可能只写入部分数据,这时你需要处理这种部分写入的情况。


(3)lseek

在Linux或Unix系统中,lseek是一个系统调用,用于改变打开文件的当前读/写位置。这个读/写位置通常被称为文件的“偏移量”。以下是lseek函数的原型:

1
2
3
#include <unistd.h>

off_t lseek(int fd, off_t offset, int whence);

lseek函数接收三个参数:

  • fd:要改变偏移量的文件描述符。这通常是一个先前使用open函数打开的文件的文件描述符。
  • offset:要移动的字节数。这个值可以是负数,表示向后移动。
  • whence:移动的基点,它可以是以下三个值之一:SEEK_SET(从文件开始处移动),SEEK_CUR(从当前位置移动),SEEK_END(从文件结束处移动)。

lseek函数返回新的文件偏移量。如果发生错误,它将返回-1,并设置全局变量errno以指示发生了什么错误。

以下是一个简单的示例,该示例使用lseek函数跳过一个文件的前100字节:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <fcntl.h>
#include <unistd.h>

int main() {
int fd = open("test.txt", O_RDONLY);
if (fd == -1) {
/* Handle error */
} else {
off_t newOffset = lseek(fd, 100, SEEK_SET);
if (newOffset == -1) {
/* Handle error */
} else {
/* Now the next read or write will start 100 bytes into the file. */
}
close(fd);
}
return 0;
}

在这个示例中,我们打开一个名为test.txt的文件,然后使用lseek函数将文件的偏移量设置为100。这意味着下一次readwrite操作将从文件的第100字节开始。如果lseek调用失败,我们需要处理该错误。

使用lseek函数时,务必检查其返回值,以确保操作已成功完成。

off_t

off_t 是用于表示文件偏移量或者大小的类型。这种类型是符号整型,因此可以表示正数、零和负数。off_t 常常被用于文件I/O操作

根据你的系统和编译器,off_t 可能会有不同的大小。在许多系统中,off_t 是一个 64 位的类型,这样它就可以表示超过 2GB 的文件大小和偏移量。但是在一些旧的或者嵌入式的系统中,off_t 可能只有 32 位


(4)模拟cp 指令将 src文件 复制到dest文件中
1
cp src dest

mycpy2.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
#include <stdio.h>
#include <stdlib.h>

#include <fcntl.h>
#include <unistd.h>



#define BUFSIZE 1024

int main(int argc,char **argv)
{
// 定义源文件以及目标文件的文件描述符(整型数字)
int sfd,dfd;
char buf[BUFSIZE];
int len,ret;

// 命令行输入的参数数量不够
if(argc < 3)
{
fprintf(stderr,"USage ...\n");
exit(1);
}

// 打开文件返回文件描述符
// 源文件(必须先存在)
sfd = open(argv[1],O_RDONLY);
if (sfd==-1)
{
// 打开失败 perror 函数关联了全局变量errno
perror("open()");
exit(1);
}

// 打开目标文件,目标文件写入O_WRONLY,不需要一定存在,不存在则创建O_CREAT,而且若存在则需要清空进行重写截断O_TRUNC
dfd = open(argv[2],O_WRONLY|O_CREAT|O_TRUNC);
if(dfd == -1)
{
// 打开失败
perror("open()");
close(sfd); // 关闭源文件,防止产生内存泄漏
exit(1);
}

while(1)
{
// 开始读取
len=read(sfd,buf,BUFSIZE);
if(len == -1)
{
// 读取失败
perror("read()");
break;
}
if(len == 0)
{
// 当你读到文件的末尾时,`read`函数将**返回0,表示没有更多的数据可以读取
// 读完了
break;
}

// 开始向目标文件写入数据
ret = write(dfd,buf,len);
if(ret == -1)
{
// 写入失败
perror("write()");
break;
}
}

close(sfd);
close(dfd);
exit(0);
}

创建源文件src.txt

1
2
123456789
今天又是元气满满的一天呢!

使用命令

1
$ ./mycpy2 src.txt dest.txt

打开dest.txt

1
2
123456789
今天又是元气满满的一天呢!

成功

BUG: 但是实际上,上面的程序存在bug,因为在向目标文件写入数据的时候,假设我们需要写入10个字节,但是实际只写入了3个字节,那么ret返回值==3,也是非负值,此时并没有写入全部数据数据,下一次还会继续进行写入,但是第二次写入会将第一次写入的数据进行截断覆盖,因此做以下修正

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
#include <stdio.h>
#include <stdlib.h>

#include <fcntl.h>
#include <unistd.h>



#define BUFSIZE 1024

int main(int argc,char **argv)
{
// 定义源文件以及目标文件的文件描述符(整型数字)
int sfd,dfd;
char buf[BUFSIZE];
int len,ret,pos;

// 命令行输入的参数数量不够
if(argc < 3)
{
fprintf(stderr,"USage ...\n");
exit(1);
}

// 打开文件返回文件描述符
// 源文件(必须先存在)
sfd = open(argv[1],O_RDONLY);
if (sfd==-1)
{
// 打开失败 perror 函数关联了全局变量errno
perror("open()");
exit(1);
}

// 打开目标文件,目标文件写入O_WRONLY,不需要一定存在,不存在则创建O_CREAT,而且若存在则需要清空进行重写截断O_TRUNC
dfd = open(argv[2],O_WRONLY|O_CREAT|O_TRUNC);
if(dfd == -1)
{
// 打开失败
perror("open()");
close(sfd); // 关闭源文件,防止产生内存泄漏
exit(1);
}

while(1)
{
// 开始读取
len=read(sfd,buf,BUFSIZE);
if(len == -1)
{
// 读取失败
perror("read()");
break;
}
if(len == 0)
{
// 当你读到文件的末尾时,`read`函数将**返回0,表示没有更多的数据可以读取
// 读完了
break;
}

pos = 0;
while(len > 0)
{
// 开始向目标文件写入数据
// 之后每次写入都从buf数组中的buf+pos位置写入
ret = write(dfd,buf+pos,len);
if(ret == -1)
{
// 写入失败
perror("write()");
exit(1); // 此时会存在内存泄漏
}
pos += ret;
len -= ret;
}
}

close(sfd);
close(dfd);

exit(0);
}

执行程序成功




三、标准IO与系统文件IO的区别关系

1.区别关系

视频教程

标准IO是依赖系统文件IO实现的

标准IO:在 C 语言中,标准 I/O 提供了许多函数,如 fopenfclosefreadfwriteprintfscanf。这些函数是通过一个 FILE 指针进行操作的,这个指针指向一个包含所有相关状态信息的对象。

标准 I/O 通常处理的三个主要的 I/O 流是:

  • 标准输入(stdin):通常来自键盘的输入数据流。
  • 标准输出(stdout):输出到终端或另一个程序的正常输出数据流。
  • 标准错误输出(stderr):输出到终端或另一个程序的错误或诊断消息。

系统IO:系统 I/O,或称为低级 I/O,是一种用于执行输入/输出操作的接口。这些操作通常涉及到数据在内存和硬盘或其他I/O设备(例如,键盘或鼠标)之间的传输。在许多操作系统中,这些I/O操作是通过系统调用实现的。

以下是一些常见的系统 I/O 函数:

  • **open()**:打开一个文件或设备,以便进行读取或写入操作。成功时,返回一个文件描述符,这是一个非负整数,用于标识操作系统中打开的特定文件或设备。失败时,返回-1。

  • **close()**:关闭一个先前打开的文件或设备。如果成功,返回0。如果失败,返回-1。

  • **read()**:从一个打开的文件或设备读取数据。返回实际读取的字节数,或者在出现错误或达到文件末尾时返回-1。

  • **write()**:向一个打开的文件或设备写入数据。返回实际写入的字节数,或者在出现错误时返回-1。

  • **lseek()**:改变一个打开的文件的当前读/写位置。

  • **fstat()stat()**:获取关于文件的信息,例如它的大小,所有者,创建时间等。

以上函数都属于底层I/O,直接与操作系统交互,没有经过任何缓冲处理,速度较快,但是使用起来比较复杂。另外,这些函数都是非标准的,因此在不同的操作系统之间可能存在差异。

标准IO与系统IO的区别

  • 层级:系统 I/O 是操作系统提供的底层接口,常见的有 open,read,write,close 等系统调用。标准 I/O 是建立在系统 I/O 之上的库级接口,它是由 C 语言的标准库提供的,例如 fopen, fread, fwrite, fclose 等函数
  • 缓冲机制标准 I/O 提供了缓冲机制,而系统 I/O 并不提供。这意味着,当你使用标准 I/O 函数写入数据时,数据可能首先被写入到一个内部缓冲区,然后在适当的时候才会被写入到实际的文件或设备。这可以提高 I/O 性能,因为许多小的 I/O 操作通常比一次大的 I/O 操作更昂贵。缓冲区满或者调用 fflush()函数时才会真正进行系统调用。而系统 I/O 每次调用 read 或 write 都直接进行系统调用。(标准IO的吞吐量大,而系统IO响应速度快)
  • 跨平台性:由于标准 I/O 是由 C 语言的标准库提供的,它在不同的操作系统上提供了一致的接口,因此具有更好的跨平台性。而系统 I/O 则强依赖于特定的操作系统
  • 文件描述符和 FILE 结构体:系统 I/O 使用的是文件描述符,它是一个非负整数。而标准 I/O 则使用的是 FILE 结构体,它是一个包含了缓冲区、状态指示器、文件位置指示器等信息的复杂结构
  • 错误处理:系统 I/O 通常通过返回 -1 并设置全局变量 errno 来报告错误,而标准 I/O 则提供了一些额外的错误检测和报告机制
  • 功能:系统 I/O 提供的功能更为底层且强大,例如支持异步 I/O,scatter/gather I/O 等。而标准 I/O 更注重普通文件读写操作的便利性。


2.标准IO与系统IO不能混用

如下程序:

test2.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main()
{
// putchar 是标准IO
// write 是系统IO
putchar('a');
write(1,"b",1);

putchar('a');
write(1,"b",1);

putchar('a');
write(1,"b",1);

putchar('\n');

exit(0);
}

make运行输出

1
2
(base) zxz@zxz-B660M-GAMING-X-AX-DDR4:~/Proj/CLionProj/UNIX/IO$ ./test2
bbbaaa

可以看到是先输出的bbb 然后输出的aaa,这个也反映了标准IO的缓冲机制与响应速度,系统IO是没用缓冲机制的

(1)strance

在终端中可以使用strance命令查看一个可执行文件的系统调用是如何发生的

1
strance ./test2

image-20230723094225249

看到上图,putchar标准IO输出都是依赖底层的系统调用IO write实现,putchar('a')先将数据放入了缓冲区

,然后最后等待缓冲区进行刷新时,将三个aaa全部交给了write写入输出IO中,进行输出,而使用write(1,"b",1)则没用缓冲机制,直接进行输出


(2)fileno与fdopen

filenofdopen 是 C 语言库中的两个用于处理文件 I/O 的函数。它们在处理涉及文件描述符和 FILE * 流之间的转换时非常有用

  • fileno

此函数用于获取与给定的 FILE * 流关联的文件描述符。该函数接收一个 FILE * 参数,并返回一个整型的文件描述符。如果出现错误,此函数将返回 -1 并设置 errno 以指示错误的类型。

示例:

1
2
3
4
5
6
7
8
9
FILE* fp = fopen("example.txt", "r");
if (fp == NULL) {
// Handle error
}
int fd = fileno(fp);
if (fd == -1) {
// Handle error
}
// Now you can use fd in functions that require a file descriptor
  • fdopen

此函数用于根据已存在的文件描述符创建一个 FILE * 流。此函数接收两个参数:一个文件描述符和一个模式字符串(如 “r”、”w”、”a” 等)。如果成功,此函数将返回一个新的 FILE * 流。如果出现错误,它将返回 NULL 并设置 errno 以指示错误的类型。

示例:

1
2
3
4
5
6
7
8
9
int fd = open("example.txt", O_RDONLY);
if (fd == -1) {
// Handle error
}
FILE* fp = fdopen(fd, "r");
if (fp == NULL) {
// Handle error
}
// Now you can use fp in functions that require a FILE *

总的来说,这两个函数可以在系统级 I/O(文件描述符)和库级 I/O(FILE * 流)之间建立桥梁,使得两种 I/O 方式能够在一定程度上互换使用



四、文件共享

视频教程

多个任务同时操作一个文件或者协同完成任务

1.删除文件的第10行代码实现

truncateftruncate 是 UNIX 系统中的两个系统调用,用于调整文件的大小

(1)truncate

此函数用于将指定路径的文件大小设置为指定的长度。如果文件原来的大小大于这个长度,那么超出的数据会被丢弃。如果文件原来的大小小于这个长度,那么文件大小会被扩展,并且新增的部分将会被填充为零字节

1
int truncate(const char *path, off_t length);
  • path 是要调整大小的文件的路径
  • length 是要设置的新的文件大小

return: 函数成功时返回 0,失败时返回 -1,并设置 errno 表示错误


(2)ftruncate

此函数用于将已打开的文件(由文件描述符指定)的大小设置为指定的长度。它的功能和 truncate 相同,但是它操作的是通过文件描述符指定的文件,而不是路径指定的文件

1
int ftruncate(int fd, off_t length);
  • fd 是已打开文件的文件描述符
  • length 是要设置的新的文件大小

return: 函数成功时返回 0,失败时返回 -1,并设置 errno 表示错误


(3)代码实现

程序地址




五、原子操作

原子操作: 不可以分割的最小单位

原子操作的作用:解决竞争和冲突

多进程与多线程并发的时候,可以使用原子操作,需要将操作原子化




六、程序中的重定向:dup与dup2

视频教程

示例程序:

1
2
3
4
5
6
7
#include <stdio.h>

int main()
{
puts("!hello");
return 0;
}

终端运行输出:

image-20230724193116286

我们可以知道puts的输出为标准输出即对应于预定义的文件描述符1,但是我们如何操作可以使得,puts输出不在标准输出中(显示在终端上),重定向其输出位置

之前我们知道,通常情况下,在文件描述符中,每个 Unix 或类 Unix 进程都会有三个预定义的文件描述符:标准输入(0)、标准输出(1)、和标准错误输出(2)

方法一:

我们可以将标准输出的文件描述符 关闭close(1),而文件描述符是优先使用可用范围内最小的内容,若此时重新open一个文件,并且返回其文件描述符,那么这个文件描述符就是1,我们再使用puts函数进行输出仍然是输出至文件描述符1所对应的位置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>
#include <stdlib.h>

#include <fcntl.h>
#include <unistd.h>

#define FILENAME "/tmp/out.txt"
int main()
{
int fd;
close(1);
fd = open(FILENAME,O_WRONLY|O_CREAT|O_TRUNC,0600);
if (fd == -1)
{
perror("open()");
exit(1);
}



puts("!hello");
return 0;
}

编译执行

1
$ gedit /tmp/out

image-20230724195036719



1.dup

dup 函数接受一个打开的文件描述符 oldfd 作为参数,并返回一个新的文件描述符。新的文件描述符将会是当前可用的最小的整数值。

1
2
3
#include <unistd.h>

int dup(int oldfd);


2.dup2

dup2 函数接受两个参数 oldfdnewfd。它会先关闭 newfd(如果 newfd 已经打开的话),然后将 oldfd 的复制赋值给 newfd。如果 oldfdnewfd 相同,那么 dup2 什么也不做并返回 newfd

1
2
3
#include <unistd.h>

int dup2(int oldfd, int newfd);

这两个函数都会设置新的文件描述符的文件状态标志(例如非阻塞标志等)为 oldfd 相同的值,并且新的文件描述符和 oldfd 将会共享同一个文件偏移量和同一个打开文件(也就是说,如果通过其中一个文件描述符进行了写操作,那么在另一个文件描述符看来文件的内容和偏移量都会改变)。

如果成功,它们都会返回新的文件描述符。如果出错,它们都会返回 -1 并设置 errno 为相应的错误号。




七、fcntl与iocntl

/dev/fd/目录: 虚目录 显示的是当前进程的文件描述符信息

1.fcntl

fcntl 是 Unix/Linux 系统调用,它的名字来源于 “file control”,用于对文件描述符进行各种操作

1
2
3
4
#include <unistd.h>
#include <fcntl.h>

int fcntl(int fd, int cmd, ... /* arg */ );

这个函数接受一个文件描述符 fd,一个命令 cmd,以及可能需要的额外参数(这取决于 cmd 的值)。它的返回值取决于 cmd 的值,如果出错,它会返回 -1 并设置 errno

fcntl 支持很多命令,下面是一些最常用的:

  • F_DUPFD:复制一个文件描述符,和 dup 功能类似,但你可以指定新的文件描述符的最小值。
  • F_GETFDF_SETFD:获取和设置文件描述符标志,如 close-on-exec。
  • F_GETFLF_SETFL:获取和设置文件状态标志,如阻塞和非阻塞I/O,append模式等。
  • F_GETLK, F_SETLKF_SETLKW:检查,设置或释放文件锁。

注意,fcntl 对于非文件类型的文件描述符(例如,sockets 和 pipes)可能会有不同的行为,或者可能不支持所有的命令。

这是一个简单的使用 fcntl 进行非阻塞I/O设置的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>

int main() {
int flags = fcntl(STDIN_FILENO, F_GETFL, 0);
if (flags == -1) {
perror("fcntl F_GETFL");
return 1;
}

flags |= O_NONBLOCK;
if (fcntl(STDIN_FILENO, F_SETFL, flags) == -1) {
perror("fcntl F_SETFL");
return 1;
}

// Now reads from stdin will not block.
// ...

return 0;
}


2.iocntl

ioctl 是 Unix/Linux 系统调用,用于设备特定的输入/输出操作。它的名字是 “input/output control” 的缩写。ioctl 函数提供了一种通用的方式来做一些不能用常规的系统调用做的事情,特别是和设备驱动程序交互的事情。

1
2
3
#include <sys/ioctl.h>

int ioctl(int fd, unsigned long request, ...);

这个函数接受一个文件描述符 fd,一个请求码 request,和零个或多个额外的参数。额外参数的数量和类型取决于 request 的值。ioctl 的返回值通常取决于 request,如果出错,它会返回 -1 并设置 errno

ioctl 的请求码和额外参数完全取决于特定的设备驱动程序。例如,终端设备(如你的 shell)提供了很多 ioctl 操作来获取和设置各种终端属性。这是一个简单的例子,它使用 ioctl 来获取终端的窗口大小:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <sys/ioctl.h>
#include <stdio.h>

int main() {
struct winsize ws;
if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == -1) {
perror("ioctl TIOCGWINSZ");
return 1;
}

printf("window size: %d rows, %d columns\n", ws.ws_row, ws.ws_col);
return 0;
}

请注意,ioctl 函数和 fcntl 函数都可以对文件描述符进行控制,但是 ioctl 更为特定于设备,通常用于处理设备驱动程序提供的更复杂的情况。而 fcntl 主要用于处理文件I/O的通用属性,如文件锁,读写模式等