UNIX 文件I/O
引言
- 介绍UNIX系统可用的文件I/O函数---打开文件、读文件、写文件等
- UNIX文件I/O常用函数:
open
、read
、write
、lseek
和close
,不同缓冲长度对read
和write
的影响 -
不带缓冲的I/O,每个
read
和write
都调用内核中的一个系统调用 - 多进程资源共享,原子操作
文件描述符
- 对于内核而言,所有打开的文件都通过文件描述符引用。文件描述符是一个非负整数。
- 依据惯例,标准输入、标准输出、标准错误的文件描述符分别为0(
STFDIN_FILENO
)、1(STFDOUT_FILENO
)、2(STFDERR_FILENO
),这些常量定义在头文件<unistd.h>
中。 - 文件描述符的变化范围是
0~OPEN_MAX-1
,早起UNIX支持的上限为19(允许每个进程最多打开20个文件),现在很多系统上限增加至63.
函数open和openat
-
open
或openat
函数可以打开或创建一个文件。#include<fcntl.h> int open(const char * path, int oflag, ...); int openat(int fd, const char * path, int oflag, ...);
- 函数最后的
...
代表可变参数类型和数量,对于open
函数来说,仅当创建新文件时才使用最后的这个参数 -
path
是要打开或者创建文件的名字 -
oflag
是打开文件的读写模式,常量定义在<fcntl.h>
中,可以使用一个或者多个常量进行“或”运算。- 必选常量【必须选一个,且只能选一个】
O_RDONLY
(只读打开)、O_WRONLY
(只写打开)、O_RDWR
(读写打开)、O_EXEC
(只执行打开)、O_SEARCH
(只搜索打开,应用于目录) - 可选常量
- 必选常量【必须选一个,且只能选一个】
-
fd
参数把open
和openat
函数区别开-
path
指定的是绝对路径,则fd
参数被忽略,openat
函数就相当于open
函数 -
path
参数是相对路径,fd
参数指出相对路径名在文件系统中的开始地址。fd
参数是通过打开相对路径名所在的目录来获取 -
path
参数是相对路径,fd
参数具有特殊值AT_FDCWD
。此时,路径名在当前工作目录获取,openat
函数在操作上与open
函数类似
-
-
openat
函数希望解决的问题- 1.让线程可以使用相对路径名打开目录中的文件,而不再只能打开当前工作目录
- 2.避免
time-of-check-to-time-of-use(TOCTTOU)
错误-
TOCTTOU
错误基本思想:如果两个基于文件的函数调用,其中第二个依赖于第一个,两个调用不是原子操作,如果在两个函数调用之间文件改变了,那第一个函数调用的结果就可能不再有效,则后面的调用可能在错误的结果上。文件系统的TOCTTOU
错误通常处理那些颠覆文件系统权限的小把戏,这些小把戏通过骗取特权文件程序降低特权文件的权限控制或者让特权文件打开一个安全漏洞等方式进行。
-
- 文件名和路径名截断
- 如果文件名或者路径名超过系统最大允许长度
NAME_MAX
(大多数等于255),常有两种方式处理-
- 将文件名截断到最大支持长度
- 这样处理将会影响文件名,且到达最大长度的文件名是否曾经被截断过也无法判断
- 返回出错状态
ENAMETOOLONG
- 返回出错状态
-
- 在
POSIX.1
中通过设置常量_POSIX_NO_TRUNC
决定截断过长的文件名或者路径名还是返回错误状态
- 如果文件名或者路径名超过系统最大允许长度
- 函数最后的
函数create
-
调用
create
函数创建一个新文件#include<fcntl.h> int creat(const char *path, mode_t mode)
等价于
open(path,O_WRONLY|O_CREATE|O_TRUNC,mode)
早起的
open
函数不支持创建文件,现在open
函数提供了相应的支持,实际上也就不再需要单独的create
函数mode
,文件访问权限,将在下一节介绍create
函数的缺点:只能以只写方式打开所创建的文件,如果要读写文件,必学先create
、close
,然后再open
,用调用open
函数实现:
open(path,O_RDWR|O_CREAT|O_TRUNC,mode);
函数close
close函数关闭一个打开文件,释放该进程加在该文件上的所有记录锁
-
当一个进程终止时,内核自动关闭它所有的打开文件
#include<unistd.h> int close(int fd);
函数lseek
当前文件偏移量:每个打开文件都有一个与其相关联的偏移量,用以度量从文件开始处计量的字节数
除非指定
O_APPEND
参数,否则偏移量为0-
lseek
函数显示地为一个打开文件设置偏移量#include<unistd.h> off_t lseek(int fd, off_t offset, int whence);
-
whence
(从何处)参数与offset
参数-
whence=SEEK_SET
:从文件开始处offset个字节; -
whence=SEEK_CUR
:从文件当前偏移值加上offset,offset可正可负; -
whence=SEEK_END
:从文件末尾偏移值加上offset,offset可正可负。
-
lseek
仅将当前文件的偏移量记录在内核中,它并不引起I/O操作。该偏移量用于下一次读或写操作。文件的偏移量可以大于文件的长度,下一次写操作将加长文件的长度,并在文件中构成一个空洞,位于文件中但没有写过的字节都被读作0,空洞不要求在磁盘上占用存储区,具体处理方式与文件系统的实现有关。
#include<apue.h>
#include<error.h>
#include<fcntl.h>
char buf1[] = "abcdefghij";
char buf2[] = "ABCDEFGHIJ";
int main() {
int fd;
if ((fd = creat("file.hole", FILE_MODE)) < 0)
err_sys("create error");
if (write(fd, buf1, 10) != 10)
err_sys("buf1 write error");
if (lseek(fd, 16384, SEEK_SET) == -1)
err_sys("lseek error");
if (write(fd, buf2, 10) != 10)
err_sys("buf2 write error");
exit(0);
}
```shell
(base) yuqdeiMac:3_fileio yuq$ ls -l file.hole
-rw-r--r-- 1 yuq staff 16394 Jun 1 21:30 file.hole
(base) yuqdeiMac:3_fileio yuq$ od -c file.hole
0000000 a b c d e f g h i j \0 \0 \0 \0 \0 \0
0000020 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0
*
0040000 A B C D E F G H I J
0040012
```
函数read
- 从打开文件中读取数据,返回读到的字节数,若到末尾返回0,失败返回-1
#include<unistd.h>
ssize_t read(int fd, void *buf, size_t nbytes);
-
实际读到的字节数少于要求读的字节数:
- 读到要求的字节数之前已经到文件末尾;
- 从终端读取时,通常一次最多读一行(APUE第18章讲述如何改变这一点)
- 从网络读取时,网络中的缓冲机制可能造成返回值小于所要求读的字节数
- 从管道或
FIFO
读时,如若管道中包含的字节少于所需的数量,那么read将返回实际可用的字节数 - 当从某些面向记录的设备读时,一次最多返回一个记录
- 当一信号造成中断,而已经读了部分数据量时
-
read
函数的经典定义int read(int fd, char *buf, unsigned nbytes);
-
POSIX.1
对经典定义进行了更改- 将
char *
改为void *
。在ISO C
中,void *
表示通用指针; - 返回值必须是一个带符号整型(
ssize_t
) - 第3个参数历史上是一个无符号整型,
size_t
不带符号
- 将
-
函数write
-
打开文件写数据,若成功,返回写的字节数,若失败,返回-1
#include<unistd.h> ssize_t write(int fd, const void *buf, size_t nbytes);
返回值通常与参数
nbytes
的值相同,否则表示出错write
函数常见出错的原因:1.磁盘写满;2.超过一个给定进程的文件长度限制;
I/O的效率
#include<apue.h>
#include<error.h>
#define BUFFSIZE 4096
int main(){
int n;
char buf[BUFFSIZE];
while((n=read(STDIN_FILENO,buf,BUFFSIZE))>0)
if(write(STDOUT_FILENO,buf,n)!=n)
err_sys("write error");
if(n<0)
err_sys("read error");
exit(0);
}
- 上面这个程序应注意以下几点:
- 它从标准输入读,写至标准输出。标准输入、输出、错误自动打开,允许用户利用shell的I/O重定向功能;
- 进程终止时,UNIX系统内核会自动关闭进程的所有打开的文件描述符,所以此程序不必关闭输入和输出文件;
- 对UNIX系统内核而言,文本文件与二进制代码文件并无区别。
-
如何选取
BUFFSIZE
?- linux ext4文件系统的磁盘块长度为4096,系统CPU时间的几个最小值也差不多会出现在BUFFSIZE为4096及以后的位置,继续增加缓冲区长度对此时间几乎没有影响
- 大多数文件系统为改善性能都采用某种预读技术,当检验到正进行顺序读取时,系统就试图读入比应用要求更多的数据,并假想应用很快就会读这些数据。缓冲区小至32字节时时钟时间与拥有较大缓冲区长度时的时钟时间几乎一样
文件共享
- UNIX系统支持在不同进程间共享打开文件
- 内核使用3种数据结构打开文件,他们之间的关系决定了在文件共享方面一个进程对另一个进程可能产生的影响。
-
每个进程在进程表中都有一个记录项,记录项种包含一张打开文件描述符表。与每个文件描述符表相关联的是:
- 文件描述符标志
- 指向一个文件表项的指针
-
内核为所有打开文件维持一张文件表
- 文件状态标志(读、写、追加、同步和非阻塞等)
- 当前文件偏移量
- 指向该文件v节点表项的指针
-
每个打开文件(或设备)都有一个v节点(v-node)结构。v节点包含了文件类型和对此文件进行各种操作函数的指针。对于大多数文件,v节点还包含了该文件的i节点(i-node,索引节点)。这些信息是在打开文件时从磁盘读入内存的,所以,文件的相关信息都是随时可用的。例如:i节点包含了文件的所有者、文件长度、指向文件实际数据块在磁盘上所在位置的指针等。
创建v节点的目的是对一个计算机系统上的多文件系统类型提供支持。Sun把这种文件系统称为虚拟文件系统,把与文件系统无关的i节点部分称为v节点
-
如果两个独立的进程各自打开了同一个文件,则有下图所示关系。
-
- 前文所述函数的进一步说明
-
write
函数:每次写入完成后,文件表项中的当前文件偏移量增加所写入的字节数。如果当前文件偏移量超过文件长度,则将i节点表项中的当前文件长度相应更新; -
O_APPEND
:将打开标志更新到文件表项中的文件状态标志中,以追加操作打开文件时,将i节点表项中的当前文件长度设置到文件表项中的当前文件偏移量中; -
lseek
:若利用lseek
定位到文件尾端,则文件表项中的当前文件偏移量首先会被设置为i节点表项中的当前文件长度(注意,这与用O_APPEND
打开文件是不同的) -
lseek
函数只修改文件表项中的当前文件偏移量,不进行任何I/O
操作
-
原子操作
原子操作是指由多步组成的一个操作。如果该操作原子的执行,则要么执行完所有步骤,要么一步也不执行。
任何要求多于一个函数调用的操作都不是原子操作
-
先定位再执行I/O不是原子操作,UNIX的XSI扩展
pread函数
和pwrite函数
允许原子性地定位并执行I/O#include<unistd.h> ssize_t pread(int fd, void *buf, size_t nbytes, off_t offset); ssize_t pwrite(int fd, const void *buf, size_t nbytes, off_t offset);
pread
:相当于调用lseek后调用read,返回读到的字节数,若到文件末尾,返回0;若出错,返回-1;pwrite
:相当于调用lseek后调用write,返回写入的字节数,若出错,返回-1;追加到一个文件需要原子操作,否则多进程处理同一个文件时,先定位得到的偏移值可能由于其他进程的操作导致失效。以
O_APPEND
标志打开文件是一种原子操作方法。-
创建一个文件的
O_CREAT
和O_EXCL
选项,也是一个原子操作。if((fd = open(pathname, O_WRONLY))<0){ if(errno==ENOENT){ if((fd = creat(path,mode))<0) err_sys("create error"); }else{ err_sys("open error"); } }
- 如果在
open
和creat
之间,另一个进程创建了该文件,并写入了一些数据,而此进程又执行creat
函数,那么,之前进程写入的数据将会被擦去。
- 如果在
函数dup和dup2
-
用来复制一个现有的文件描述符。若成功,返回新的文件描述符,若失败,返回-1
#include<unistd.h> int dup(int fd); int dup2(int fd, int fd2);
dup
:返回的新文件描述符一定是当前可用文件描述符中的最小数值dup2
:可以用fd2
指定新描述符的值。如果fd2
已经打开,则先关闭;如果fd2
等于fd
,则返回fd2
,而不关闭它。否则,fd2
的FD_CLOEXEC
文件描述符标志就被清除,这样fd2
在进程调用exec
时是打开状态。-
这些函数返回的新文件描述符与参数
fd
共享同一个文件表项
每个文件描述符都有它自己的一套文件描述符标志。文件描述符的
执行时关闭(close-on-exec)
标志总是由dup函数清除(后面会提到)。-
fcntl
函数:另一种复制描述符的方法dup(fd); //等价于 fcntl(fd,F_DUPFD,0); // dup2(fd,fd2); //等价于(不完全等价,前面是原子操作,后面不是,而且dup2与fcntl有一些不同的errno) close(fd2); fcntl(fd,F_DUPFD,fd2);
函数sync、fsync和fdatasync
- 延迟写:当我们向文件写入数据时,内核通常先将数据复制到缓冲区中,然后排入队列,晚些时候再写入磁盘。
- 通常,当内核需要重用缓冲区来存放其他磁盘块数据时,它会将所有延迟写数据块写入磁盘
-
sync
、fsync
以及fdatasync
:为了保证磁盘上的实际文件系统与缓冲区中内容的一致性
#include<unistd.h>
int fsync(int fd);
int fdatasync(int fd);
void sync(void);
-
sync
:将所有修改过的块缓冲区排入写队列,然后就返回,不等待实际写磁盘结束- 通常,称为
update
的系统守护进程周期性地调用(一般每个30s)sync
函数。这就保证定期冲洗(flush)内核
的块缓冲区。
- 通常,称为
-
fsync
:只对由文件描述符fd
指定的一个文件起作用,并且等待写磁盘操作结束才返回 -
fdatasync
:类似fsync
,但它只影响文件的数据部分,而fsync
还会同步更新文件的属性
函数fcntl
-
改变已经打开文件的属性,若成功则返回依赖于
cmd
,若出错,返回-1.#include<fcntl.h> int fcntl(int fd, int cmd, ... /* int arg */);
-
fcntl
函数的种功能- 复制一个已有的描述符(
cmd=F_DUPFD
或F_DUPFD_CLOEXEC
)-
F_DUPFD
- 复制
文件描述符fd
,结果由函数fcntl
返回 - 新文件描述符要求:尚未打开的描述符,数值大于fcntl函数的第三个整型参数
- 新文件描述符与
fd
共享同一文件表项,但是有一套自己的文件描述符标志;其FD_CLOEXEC
标志被清除(这表示该描述符在exec
时仍然有效,close exec
也就是执行时关闭的意思)
- 复制
-
F_DUPFD_CLOEXEC
- 跟
F_DUPFD
一样,复制文件描述符 - 设置与新描述符关联的
FD_CLOEXEC
文件描述符的值
- 跟
-
- 获取/设置文件描述符标志(
cmd=F_GETFD
或F_SETFD
)-
F_GETFD
- 获取
fd
的文件描述符标志。当前只定义了一个文件描述符标志FD_CLOEXEC
- 获取
-
F_SETFD
- 按照
fcntl函数
第三个整型参数设置fd
文件描述符标志 -
exec
时不关闭,FD_CLOEXEC=0
;exec
时关闭,FD_CLOEXEC=1
- 按照
-
- 获取/设置文件状态标志(
cmd=F_GETFL
或F_SETFL
)- 状态也就是读写状态;例如只读、只写、追加、同步读写等
-
O_RDONLY
(只读打开)、O_WRONLY
(只写打开)、O_RDWR
(读写打开)、O_EXEC
(只执行打开)、O_SEARCH
(只搜索打开,应用于目录);这5个标志必须选一个,也只能选一个,前三个标志分别是0、1、2
。由于这5个值互斥,因此首先使用屏蔽字O_ACCMODE
取得访问方式位,然后将结果与这5个值比较。
- 获取/设置
异步I/O
所有权(cmd=F_GETOWN
或F_SETOWN
)- 获取/设置接收
SIGIO
和SIGURG
信号的进程ID
或进程组ID
,正的arg
表示指定一个进程ID
,负的arg
表示指定一个进程组ID
(取绝对值)
- 获取/设置接收
- 获取/设置记录锁(
cmd=F_GETLK
、F_SETLK
或F_SETLKW
)
#include<apue.h> #include<error.h> #include<fcntl.h> int main(int argc, char *argv[]) { int val; if (argc != 2) err_quit("usage: a.out <descriptor#>"); if ((val = fcntl(atoi(argv[1]), F_GETFL, 0)) < 0) err_sys("fcntl error for fd %d", atoi(argv[1])); switch (val & O_ACCMODE) { case O_RDONLY: printf("read only"); break; case O_WRONLY: printf("read only"); break; case O_RDWR: printf("read write"); break; default: err_dump("unknown access mode"); } if (val & O_APPEND) printf(", append"); if (val & O_NONBLOCK) printf(", nonblocking"); if (val & O_SYNC) printf(", synchronous writes"); #if !defined(_POSIX_C_SOURCE) && defined(O_FSYNC) && (O_FSYNC != O_SYNC) if(val & O_FSYNC) printf(", synchronous writes"); #endif putchar('\n'); exit(0); }
- 修改文件描述符标志或文件状态标志
- 先获得现有标志,然后按照期望设置(位操作)
- 直接使用
F_SETFD
或F_SETFL
会关闭以前设置的标志
#include<apue.h> #include<error.h> #include<fcntl.h> void set_fl(int fd, int flags) { int val; if ((val = fcntl(fd, F_GETFL, 0)) < 0) err_sys("fcntl F_GETFL error"); val |= flags;//开启一个文件状态标志 if (fcntl(fd, F_SETFL, val) < 0) err_sys("fcntl F_SETFL error"); } void clr_fl(int fd, int flags) { int val; if ((val = fcntl(fd, F_GETFL, 0)) < 0) err_sys("fcntl F_GETFL error"); val &= ~flags;//关闭一个文件状态标志 if (fcntl(fd, F_SETFL, val) < 0) err_sys("fcntl F_SETFL error"); }
-
例如使用
set_fl(STDOUT_FILENO,O_SYNC);
开启同步标志-
UNIX
系统中,单纯使用write
只是将数据排入队列,并不一定立即写入磁盘 - 在数据库等系统中,需要使用
O_SYNC
标志确保数据已经写在磁盘上,以免系统异常时丢失数据 - 而在
Linux
系统中,不允许fcntl
设置O_SYNC
,所以设置无效,但是如果文件打开时能指定该标志,仍然应该遵从这个标志 -
Linux
中设置O_SYNC
之后还应该调用fsync
,MACOS
中O_SYNC
起到了应有的作用。
-
-
fcntl
函数的种功能- 复制一个已有的描述符(
cmd=F_DUPFD
或F_DUPFD_CLOEXEC
)-
F_DUPFD
- 复制
文件描述符fd
,结果由函数fcntl
返回 - 新文件描述符要求:尚未打开的描述符,数值大于fcntl函数的第三个整型参数
- 新文件描述符与
fd
共享同一文件表项,但是有一套自己的文件描述符标志;其FD_CLOEXEC
标志被清除(这表示该描述符在exec
时仍然有效,close exec
也就是执行时关闭的意思)
- 复制
-
F_DUPFD_CLOEXEC
- 跟
F_DUPFD
一样,复制文件描述符 - 设置与新描述符关联的
FD_CLOEXEC
文件描述符的值
- 跟
-
- 获取/设置文件描述符标志(
cmd=F_GETFD
或F_SETFD
)-
F_GETFD
- 获取
fd
的文件描述符标志。当前只定义了一个文件描述符标志FD_CLOEXEC
- 获取
-
F_SETFD
- 按照
fcntl函数
第三个整型参数设置fd
文件描述符标志 -
exec
时不关闭,FD_CLOEXEC=0
;exec
时关闭,FD_CLOEXEC=1
- 按照
-
- 获取/设置文件状态标志(
cmd=F_GETFL
或F_SETFL
)- 状态也就是读写状态;例如只读、只写、追加、同步读写等
-
O_RDONLY
(只读打开)、O_WRONLY
(只写打开)、O_RDWR
(读写打开)、O_EXEC
(只执行打开)、O_SEARCH
(只搜索打开,应用于目录);这5个标志必须选一个,也只能选一个,前三个标志分别是0、1、2
。由于这5个值互斥,因此首先使用屏蔽字O_ACCMODE
取得访问方式位,然后将结果与这5个值比较。
- 获取/设置
异步I/O
所有权(cmd=F_GETOWN
或F_SETOWN
)- 获取/设置接收
SIGIO
和SIGURG
信号的进程ID
或进程组ID
,正的arg
表示指定一个进程ID
,负的arg
表示指定一个进程组ID
(取绝对值)
- 获取/设置接收
- 获取/设置记录锁(
cmd=F_GETLK
、F_SETLK
或F_SETLKW
)
#include<apue.h> #include<error.h> #include<fcntl.h> int main(int argc, char *argv[]) { int val; if (argc != 2) err_quit("usage: a.out <descriptor#>"); if ((val = fcntl(atoi(argv[1]), F_GETFL, 0)) < 0) err_sys("fcntl error for fd %d", atoi(argv[1])); switch (val & O_ACCMODE) { case O_RDONLY: printf("read only"); break; case O_WRONLY: printf("read only"); break; case O_RDWR: printf("read write"); break; default: err_dump("unknown access mode"); } if (val & O_APPEND) printf(", append"); if (val & O_NONBLOCK) printf(", nonblocking"); if (val & O_SYNC) printf(", synchronous writes"); #if !defined(_POSIX_C_SOURCE) && defined(O_FSYNC) && (O_FSYNC != O_SYNC) if(val & O_FSYNC) printf(", synchronous writes"); #endif putchar('\n'); exit(0); }
- 修改文件描述符标志或文件状态标志
- 先获得现有标志,然后按照期望设置(位操作)
- 直接使用
F_SETFD
或F_SETFL
会关闭以前设置的标志
#include<apue.h> #include<error.h> #include<fcntl.h> void set_fl(int fd, int flags) { int val; if ((val = fcntl(fd, F_GETFL, 0)) < 0) err_sys("fcntl F_GETFL error"); val |= flags;//开启一个文件状态标志 if (fcntl(fd, F_SETFL, val) < 0) err_sys("fcntl F_SETFL error"); } void clr_fl(int fd, int flags) { int val; if ((val = fcntl(fd, F_GETFL, 0)) < 0) err_sys("fcntl F_GETFL error"); val &= ~flags;//关闭一个文件状态标志 if (fcntl(fd, F_SETFL, val) < 0) err_sys("fcntl F_SETFL error"); }
- 例如使用
set_fl(STDOUT_FILENO,O_SYNC);
开启同步标志-
UNIX
系统中,单纯使用write
只是将数据排入队列,并不一定立即写入磁盘 - 在数据库等系统中,需要使用
O_SYNC
标志确保数据已经写在磁盘上,以免系统异常时丢失数据 - 而在
Linux
系统中,不允许fcntl
设置O_SYNC
,所以设置无效,但是如果文件打开时能指定该标志,仍然应该遵从这个标志 -
Linux
中设置O_SYNC
之后还应该调用fsync
,MACOS
中O_SYNC
起到了应有的作用。
-
- 复制一个已有的描述符(
- 复制一个已有的描述符(
函数ioctl
-
ioctl
是I/O操作
的杂物箱,终端I/O
是使用ioctl
最多的地方
#include<unistd.h>
#include<sys/ioctl.h>
int ioctl(int fd, int request, ...);
/dev/fd
-
/dev/fd
目录下是名为0、1、2、3、4、5等
的文件,打开文件/dev/fd/n
等效于复制描述符n
(假定描述符n
是打开的) -
fd = open("dev/fd/0", mode)
- 大多数系统忽略指定的
mode
- 另一些要求
mode
必须是所引用文件初始打开时所使用的打开模式的一个子集。例如初始以只读模式打开,那么之后用读写模式打开,也不能对文件进行写操作
- 大多数系统忽略指定的
-
Linux
实现中的/dev/fd
把文件描述符映射成指向底层物理文件的符号链接。当打开与标准输入关联的文件,返回的新文件描述符模式与/dev/fd
文件描述符的模式其实并不相关
习题
-
读写磁盘文件时,本章中描述的函数确实是不带缓冲机制的吗?
- 不是,所有的磁盘I/O都要经过内核的块缓冲区(内核的缓冲区高速缓存),不带缓冲术语指的是在用户进程中对这两个函数不会自动缓冲
-
写一个和
dup2
功能相同的函数,要求不调用fcntl
函数,并且要有正确的错误处理- 网上的实现大部分如下面的代码,然而这个代码感觉有些问题,这个代码不是原子操作,在完成之前如果异常退出,则
dup函数
复制的fd1
也就没有办法删除了
#include<apue.h> #include<error.h> #include<unistd.h> typedef struct fdList { int fd; struct fdList *pre; } fdLink; int myDup2(int fd1, int fd2) { if (fd1 < 0 || fd2 < 0) err_sys("fd1 and fd2 must be non-negative integers\n"); if (fd1 == fd2) return fd2; close(fd2); fdLink *pre = NULL; fdLink *node; int fd; while ((fd = dup(fd1)) != fd2) { node=(fdLink*)malloc(sizeof(fdLink)); node->fd=fd; node->pre=pre; pre=node; } fdLink* tmp; while(pre!=NULL){ close(pre->fd); tmp=pre; pre=pre->pre; free(tmp); } return fd2; }
- 网上的实现大部分如下面的代码,然而这个代码感觉有些问题,这个代码不是原子操作,在完成之前如果异常退出,则
-
如果一个进程执行下面3个函数的调用:
fd1 = open(path, oflags); fd2 = dup(fd1); fd3 = open(path,oflags);
那么三个文件描述符之间的关系?文件表项是否共享?v节点表项的关系?
- 每次调用
open函数
就会分配一个新的文件表项 - 两次open函数打开的是同一个文件,所以指向相同的v节点
-
dup函数
引用已经存在的文件表项
- 每次调用
-
如果使用追加标志打开一个文件以便读写,能否仍用
lseek
在任一位置开始读?能否用lseek
更新文件中任一部分的数据?请编写一段程序验证。- 追加标志打开的文件执行的是原子操作,每次写都会定位到文件的末尾,所以不能更新任一部分数据
- 但是读任一部分不影响
- 测试示例:
#include <fcntl.h> #include <unistd.h> #include <apue.h> #include <error.h> int main() { int fd; if ((fd = creat("./test.txt", S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP)) < 0) err_sys("create file error\n"); char *msg = "hello world"; if (write(fd, msg, strlen(msg)) != strlen(msg)) err_sys("write error\n"); close(fd); int fd2 = open("./test.txt", O_RDWR | O_APPEND); //从开头偏移6个字母,显示读取结果 off_t cur_pos = lseek(fd2, 6, SEEK_SET); if (cur_pos == -1) err_sys("cannot seek\n"); char buf[4096]; while (read(fd2, buf, 4096) > 0) { printf("%s", buf); } printf("\n"); //从开头偏移6个字幕,执行写操作 cur_pos = lseek(fd2, 6, SEEK_SET); if (cur_pos == -1) err_sys("cannot seek\n"); char *msg2 = "! Love Coding"; write(fd2, msg2, strlen(msg2)); //将偏移调至开头,读取所有内容 cur_pos = lseek(fd2, 0, SEEK_SET); if (cur_pos == -1) err_sys("cannot seek\n"); while (read(fd2, buf, 4096) > 0) { printf("%s\n", buf); } }
- 测试输出
world hello world! Love Coding
Reference
- 《Unix环境高级编程(第3版)》