UNIX环境编程-系统调用IO(8)
UNIX环境编程-系统调用IO(8)
一、文件描述符
文件描述符的实质:一个整型数字,数组下标
在 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 |
|
open
函数接收两个或三个参数:
pathname
:文件的路径。flags
:打开文件的模式,比如O_RDONLY
(只读),O_WRONLY
(只写),O_RDWR
(读写),O_CREAT
(如果文件不存在,则创建文件),O_TRUNC
(如果文件已存在且为可写,则清空文件内容),等等。mode
:当创建新文件时(即使用了O_CREAT
标志),mode
参数用于指定新文件的权限。这是一个可选参数。
return : open
函数返回一个非负的文件描述符,如果打开文件失败,则返回-1。
以下是一个简单的示例,该示例使用open
函数打开一个文件进行写入操作:
1 |
|
在这个示例中,如果文件test.txt
不存在,那么open
函数会创建这个文件,文件的权限会被设置为0644(这意味着文件的所有者可以读写文件,其他用户只能读取文件)。然后,你可以使用返回的文件描述符fd
进行写入操作。完成写入操作后,别忘了使用close
函数关闭文件。
在使用open
函数时,务必检查其返回值以确保文件已成功打开。如果文件打开失败,open
函数将返回-1,并设置全局变量errno
以指示发生了什么错误。
2.read、write、lseek以及mycopy的实现
(1)read
在Linux或Unix系统中,read
是一个系统调用,用于从打开的文件描述符中读取数据。以下是read
函数的原型:
1 |
|
read
函数接收三个参数:
fd
:要读取的文件描述符。这通常是一个先前使用open
函数打开的文件的文件描述符。buf
:一个缓冲区,用于存储从文件描述符中读取的数据。count
:要从文件描述符中读取的字节数。
read
函数返回实际读取的字节数。如果发生错误,它将返回-1,并设置全局变量errno
以指示发生了什么错误。
在Linux或Unix系统中,read
是一个系统调用,用于从打开的文件描述符中读取数据。以下是read
函数的原型:
1 | cCopy code#include <unistd.h> |
read
函数接收三个参数:
fd
:要读取的文件描述符。这通常是一个先前使用open
函数打开的文件的文件描述符。buf
:一个缓冲区,用于存储从文件描述符中读取的数据。count
:要从文件描述符中读取的字节数。
read
函数返回实际读取的字节数。如果发生错误,它将返回-1,并设置全局变量errno
以指示发生了什么错误。
以下是一个简单的示例,该示例使用read
函数从一个文件中读取数据:
1 |
|
在这个示例中,我们打开一个名为test.txt
的文件,然后尝试从该文件中读取最多127个字节的数据(我们预留一个字节用于字符串的空字符终止符)。然后,我们在读取的数据后添加一个空字符,使其成为一个有效的C字符串。如果读取过程中发生错误,我们需要处理该错误。
使用read
函数时,务必检查其返回值,以确保没有发生错误并且已成功读取到数据。当你读到文件的末尾时,read
函数将返回0,表示没有更多的数据可以读取。
(2)write
在Linux或Unix系统中,write
是一个系统调用,用于将数据写入到打开的文件描述符。以下是write
函数的原型:
1 |
|
write
函数接收三个参数:
fd
:要写入的文件描述符。这通常是一个先前使用open
函数打开的文件的文件描述符。buf
:包含要写入的数据的缓冲区。count
:要写入的字节数。
write
函数返回实际写入的字节数。如果发生错误,它将返回-1,并设置全局变量errno
以指示发生了什么错误。
以下是一个简单的示例,该示例使用write
函数向一个文件中写入数据:
1 |
|
在这个示例中,我们打开(或创建)一个名为test.txt
的文件,然后尝试向该文件中写入一个字符串。如果写入过程中发生错误,我们需要处理该错误。我们还检查了write
函数是否已成功写入所有的数据。
使用write
函数时,务必检查其返回值,以确保没有发生错误并且所有数据都已成功写入。在某些情况下(例如,磁盘空间不足),write
函数可能只写入部分数据,这时你需要处理这种部分写入的情况。
(3)lseek
在Linux或Unix系统中,lseek
是一个系统调用,用于改变打开文件的当前读/写位置。这个读/写位置通常被称为文件的“偏移量”。以下是lseek
函数的原型:
1 |
|
lseek
函数接收三个参数:
fd
:要改变偏移量的文件描述符。这通常是一个先前使用open
函数打开的文件的文件描述符。offset
:要移动的字节数。这个值可以是负数,表示向后移动。whence
:移动的基点,它可以是以下三个值之一:SEEK_SET
(从文件开始处移动),SEEK_CUR
(从当前位置移动),SEEK_END
(从文件结束处移动)。
lseek
函数返回新的文件偏移量。如果发生错误,它将返回-1,并设置全局变量errno
以指示发生了什么错误。
以下是一个简单的示例,该示例使用lseek
函数跳过一个文件的前100字节:
1 |
|
在这个示例中,我们打开一个名为test.txt
的文件,然后使用lseek
函数将文件的偏移量设置为100。这意味着下一次read
或write
操作将从文件的第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 |
|
创建源文件src.txt
1 | 123456789 |
使用命令
1 | $ ./mycpy2 src.txt dest.txt |
打开dest.txt
1 | 123456789 |
成功
BUG: 但是实际上,上面的程序存在bug,因为在向目标文件写入数据的时候,假设我们需要写入10个字节,但是实际只写入了3个字节,那么ret返回值==3,也是非负值,此时并没有写入全部数据数据,下一次还会继续进行写入,但是第二次写入会将第一次写入的数据进行截断覆盖,因此做以下修正
1 |
|
执行程序成功
三、标准IO与系统文件IO的区别关系
1.区别关系
标准IO是依赖系统文件IO实现的
标准IO:在 C 语言中,标准 I/O 提供了许多函数,如 fopen
、fclose
、fread
、fwrite
、printf
和 scanf
。这些函数是通过一个 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 |
|
make
运行输出
1 | (base) zxz@zxz-B660M-GAMING-X-AX-DDR4:~/Proj/CLionProj/UNIX/IO$ ./test2 |
可以看到是先输出的bbb 然后输出的aaa,这个也反映了标准IO的缓冲机制与响应速度,系统IO是没用缓冲机制的
(1)strance
在终端中可以使用strance
命令查看一个可执行文件的系统调用是如何发生的
1 | strance ./test2 |
看到上图,putchar
标准IO输出都是依赖底层的系统调用IO write
实现,putchar('a')
先将数据放入了缓冲区
,然后最后等待缓冲区进行刷新时,将三个aaa
全部交给了write
写入输出IO中,进行输出,而使用write(1,"b",1)
则没用缓冲机制,直接进行输出
(2)fileno与fdopen
fileno
和 fdopen
是 C 语言库中的两个用于处理文件 I/O 的函数。它们在处理涉及文件描述符和 FILE *
流之间的转换时非常有用
- fileno
此函数用于获取与给定的 FILE *
流关联的文件描述符。该函数接收一个 FILE *
参数,并返回一个整型的文件描述符。如果出现错误,此函数将返回 -1 并设置 errno 以指示错误的类型。
示例:
1 | FILE* fp = fopen("example.txt", "r"); |
- fdopen
此函数用于根据已存在的文件描述符创建一个 FILE *
流。此函数接收两个参数:一个文件描述符和一个模式字符串(如 “r”、”w”、”a” 等)。如果成功,此函数将返回一个新的 FILE *
流。如果出现错误,它将返回 NULL 并设置 errno 以指示错误的类型。
示例:
1 | int fd = open("example.txt", O_RDONLY); |
总的来说,这两个函数可以在系统级 I/O(文件描述符)和库级 I/O(FILE *
流)之间建立桥梁,使得两种 I/O 方式能够在一定程度上互换使用
四、文件共享
多个任务同时操作一个文件或者协同完成任务
1.删除文件的第10行代码实现
truncate
和 ftruncate
是 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 |
|
终端运行输出:
我们可以知道puts
的输出为标准输出即对应于预定义的文件描述符1
,但是我们如何操作可以使得,puts
输出不在标准输出中(显示在终端上),重定向其输出位置
之前我们知道,通常情况下,在文件描述符中,每个 Unix 或类 Unix 进程都会有三个预定义的文件描述符:标准输入(0)、标准输出(1)、和标准错误输出(2)
方法一:
我们可以将标准输出的文件描述符 关闭close(1)
,而文件描述符是优先使用可用范围内最小的内容,若此时重新open
一个文件,并且返回其文件描述符,那么这个文件描述符就是1,我们再使用puts
函数进行输出仍然是输出至文件描述符1
所对应的位置
1 |
|
编译执行
1 | $ gedit /tmp/out |
1.dup
dup
函数接受一个打开的文件描述符 oldfd
作为参数,并返回一个新的文件描述符。新的文件描述符将会是当前可用的最小的整数值。
1 |
|
2.dup2
dup2
函数接受两个参数 oldfd
和 newfd
。它会先关闭 newfd
(如果 newfd
已经打开的话),然后将 oldfd
的复制赋值给 newfd
。如果 oldfd
和 newfd
相同,那么 dup2
什么也不做并返回 newfd
。
1 |
|
这两个函数都会设置新的文件描述符的文件状态标志(例如非阻塞标志等)为 oldfd
相同的值,并且新的文件描述符和 oldfd
将会共享同一个文件偏移量和同一个打开文件(也就是说,如果通过其中一个文件描述符进行了写操作,那么在另一个文件描述符看来文件的内容和偏移量都会改变)。
如果成功,它们都会返回新的文件描述符。如果出错,它们都会返回 -1 并设置 errno
为相应的错误号。
七、fcntl与iocntl
/dev/fd/
目录: 虚目录 显示的是当前进程的文件描述符信息
1.fcntl
fcntl
是 Unix/Linux 系统调用,它的名字来源于 “file control”,用于对文件描述符进行各种操作
1 |
|
这个函数接受一个文件描述符 fd
,一个命令 cmd
,以及可能需要的额外参数(这取决于 cmd
的值)。它的返回值取决于 cmd
的值,如果出错,它会返回 -1 并设置 errno
fcntl
支持很多命令,下面是一些最常用的:
F_DUPFD
:复制一个文件描述符,和dup
功能类似,但你可以指定新的文件描述符的最小值。F_GETFD
和F_SETFD
:获取和设置文件描述符标志,如 close-on-exec。F_GETFL
和F_SETFL
:获取和设置文件状态标志,如阻塞和非阻塞I/O,append模式等。F_GETLK
,F_SETLK
和F_SETLKW
:检查,设置或释放文件锁。
注意,fcntl
对于非文件类型的文件描述符(例如,sockets 和 pipes)可能会有不同的行为,或者可能不支持所有的命令。
这是一个简单的使用 fcntl
进行非阻塞I/O设置的例子:
1 |
|
2.iocntl
ioctl
是 Unix/Linux 系统调用,用于设备特定的输入/输出操作。它的名字是 “input/output control” 的缩写。ioctl
函数提供了一种通用的方式来做一些不能用常规的系统调用做的事情,特别是和设备驱动程序交互的事情。
1 |
|
这个函数接受一个文件描述符 fd
,一个请求码 request
,和零个或多个额外的参数。额外参数的数量和类型取决于 request
的值。ioctl
的返回值通常取决于 request
,如果出错,它会返回 -1 并设置 errno
。
ioctl
的请求码和额外参数完全取决于特定的设备驱动程序。例如,终端设备(如你的 shell)提供了很多 ioctl
操作来获取和设置各种终端属性。这是一个简单的例子,它使用 ioctl
来获取终端的窗口大小:
1 |
|
请注意,ioctl
函数和 fcntl
函数都可以对文件描述符进行控制,但是 ioctl
更为特定于设备,通常用于处理设备驱动程序提供的更复杂的情况。而 fcntl
主要用于处理文件I/O的通用属性,如文件锁,读写模式等