一、原子操作、竞争
原子操作:一个独立而不可分隔的操作。
所有系统调用都是以原子操作方式执行的。
原子操作规避了竞争状态。
竞争状态:操作共享资源的两个进程(或线程),其结果取决于一个无法预测的顺序,即这些进程获得CPU使用权的先后相对顺序。
1、原子操作必要性说明
思考以下程序:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#ifndef BUF_SIZE
#define BUF_SIZE 1024
#endif
int main(int argc,char *argv[])
{
int fd;
// 以只写的方式打开文件,如果文件不存在则报错
fd = open(argv[1],O_WRONLY);
if(fd != -1){
// 文件打开成功,说明文件已经存在
printf("[PID %ld] file \"%s\" already exists\n",(long)getpid(),argv[1]);
close(fd);
}else{
// 文件打开失败
if(errno != ENOENT){
// 其他原因打开文件失败
printf("open\n");
}else{
// 文件存在打开失败
printf("[PID %ld] file \"%s\" doesn't exist\n",(long)getpid(),argv[1]);
if(argc > 2){
sleep(5);
}
// 打开文件如果不存在则创建
fd = open(argv[1],O_WRONLY|O_CREAT, S_IRUSR|S_IWUSR);
if(fd == -1)
printf("open\n"); // 文件打开失败
else
printf("[PID %ld] Create file \"%s\" exclusively\n",(long)getpid(),argv[1]);
}
}
return 0;
}
通过以上执行,可以看到 2个进程都认为文件是自己创建的。
分析:
两个进程对同一个文件同时操作,产生竞争状态,使一个进程得到错误的结论。
2、以独占方式创建一个文件
在使用 open() 系统调用时,同时指定 O_EXCL 和 O_CREAT 作为 open() 的标志位,保证进程是打开文件的创建者。对文件是否存在的检查和创建文件属于同一原子操作。
二、文件描述符和打开文件之间的关系
要理解文件描述符和打开文件之间的关系,需要查看内核维护的3个数据结构:
1、进程级的文件描述符表。
2、系统级的打开文件表。
3、文件系统的 i-node 表。
1、文件描述符表
每一条目录都记录了单个文件描述符的相关信息。
- 控制文件描述符操作一组标志(目前只有一个,即 close-on-exec标志)
- 对打开文件句柄的引用
2、打开文件表
打开文件表中各条目称为打开文件句柄。一个打开文件句柄存储了与一个打开文件相关的全部信息。
- 当前文件偏移量(调用 read() 和 write() 时更新,或使用 lseek() 直接修改)。
- 打开文件时所使用的状态标志(即 open() 的 flags 参数)。
- 文件访问模式(如调用 open() 时所设置的只读模式、只写模式或读写模式)。
- 与信号驱动 I/O 相关的设置。
- 对该文件 i-node 对象的引用。
3、i-node 表
- 文件类型(例如,常规文件、套接字或FIFO)和访问权限。
- 一个指针,指向该文件所持有的锁列表。
- 文件的各种属性,包括文件大小以及与不同类型操作相关的时间戳。
i-node 在磁盘和内存中有所差异。
3、文件控制操作:fcntl()
操纵文件描述符.
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );
1、参数
1、fd
open() 系统调用返回的文件描述符。
2、cmd
命令 | 含义 |
---|---|
F_GETFL | 获取文件状态标志 |
F_SETFL | 修改打开文件的某些状态标志 |
F_DUPFD | 复制文件描述符 |
…… | …… |
- F_GETFL
判定文件的访问描述符有一点复杂,这是因为 O_RDONLY(0)、O_WRONLY(1) 和 O_RDWR(2) 这3个常量并不与打开文件状态标志中的单个比特位对应。因此,要判定访问模式,需要使用掩码 O_ACCMODE 与 flag 想与,将结果与这三个常量进行对比,示例代码如下:
access_mode = flags & O_ACCMODE;
if(access_mode == O_WRONLY || access_mode == RDWR)
printf("file is writable\n");
- F_SETFL
允许更改状态标志 O_APPEND、O_NONBLOCK、O_NOATIME、O_ASYNC和O_DIRECT,系统将忽略对其他标志的修改操作。 - F_DUPFD
newfd = fcntl(oldfd,F_DUPFD,startfd);
该系统调用为 oldfd 创建一个副本,且将使用大于等于 startfd 的最小未用值作为描述符编号。总是能将 dup() 和 dup2() 调用改写为对 close() 和 fcntl() 的调用。
- ……
2、返回值
3、备注
1、fcntl() 修改文件状态标志,适用如下场景
- 文件不是由调用程序打开的,所以程序无法使用 open() 调用来控制文件的状态标志。
- 文件描述符的获得通过 open() 之外的系统调用。
三、其他系统调用
1、dup()
#include <unistd.h>
int dup(int oldfd);
int dup2(int oldfd, int newfd);
#define _GNU_SOURCE /* See feature_test_macros(7) */
#include <fcntl.h> /* Obtain O_* constant definitions */
#include <unistd.h>
int dup3(int oldfd, int newfd, int flags)
dup() 调用复制一个打开的文件描述符 oldfd,并返回一个新文件描述符,二者都指向同一个打开的文件句柄。
dup2() 调用会为 oldfd 参数所指定的文件描述符创建副本,其编号由 newfd 参数指定。如果 newfd 参数指定文件描述符已经打开,那么 dup2() 会先将其关闭。dup2() 会忽略 newfd 关闭期间出现的任何错误。
dup3() 系统调用完成的工作与 dup2() 相同,只是增加了一个附加参数 flag,这是一个可以修改系统调用行为的位掩码。
2、pread()、pwrite()
#include <unistd.h>
ssize_t pread(int fd, void *buf, size_t count, off_t offset);
ssize_t pwrite(int fd, const void *buf, size_t count, off_t offset);
pread() 调用等同于将如下调用纳入同一原子操作:
off_t orig;
orig = lseek(fd,0,SEEK_CUR); // 保存当前文件偏移量
lseek(fd,offset,SEEK_SET);
s = read(fd,buf,len);
lseek(fd,orig,SEEK_SET); // 恢复文件偏移量
对于 pread() 和 pwrite() 而言,fd 所指代的文件必须是可定位的。
多线程应用为这些系统调用提供用武之地。进程中所有线程共享同一文件描述符表。这也意味着每个已打开的文件偏移量为所有线程共享。当调用 pread() 或 pwrite() 时,多个线程可以同时对同一文件描述符执行 IO 操作,且不会其他线程修改文件偏移量而受到影响。
3、readv()、writev()
struct iovec{
void *iov_base;
size_t iov_len;
};
#include <sys/uio.h>
ssize_t readv(int fd, const struct iovec *iov, int iovcnt);
ssize_t writev(int fd, const struct iovec *iov, int iovcnt);
ssize_t preadv(int fd, const struct iovec *iov, int iovcnt,
off_t offset);
ssize_t pwritev(int fd, const struct iovec *iov, int iovcnt,
off_t offset);
ssize_t preadv2(int fd, const struct iovec *iov, int iovcnt,
off_t offset, int flags);
ssize_t pwritev2(int fd, const struct iovec *iov, int iovcnt,
off_t offset, int flags);
1、分散输入
readv() 系统调用实现分散输入功能:从文件描述符 fd 所指代的文件中读取一片连续的字节,然后将其散置(分散放置)于 iov 指定的缓冲区中。这一散置动作从 iov[0] 开始,依次填满每个缓冲区。
原子性是 readv() 的重要属性。
2、集中输出
writev() 系统调用实现集中输出:将 iov 所指定的所有缓冲区中的数据拼接(“集中”)起来,然后以连续的字节序列写入文件描述符 fd 所指代的文件中。对缓冲区中数据 “集中” 始于 iov[0] 所指代的缓冲区,并按数组顺序展开。
原子性是 writev() 的重要属性。
3、readv() 和 writev() 主要优势
readv() 调用和 writev() 调用的主要优势在于便捷。如下两种方案,任选其一都可替代对 writev() 的调用。
4、preadv()、pwritev()
preadv() 和 pwriev() 系统调用所执行的任务于 readv() 和 writev() 相同,但执行 I/O 的位置将由 offset 参数指定(类似 pread() 和 pwrite())。
4、truncate()、ftruncate()
#include <unistd.h>
#include <sys/types.h>
int truncate(const char *path, off_t length);
int ftruncate(int fd, off_t length);
truncate() 系统调用和 ftruncate() 系统调用将文件大小设置为 length 参数指定的值。
若文件当前长度大于参数 length,调用将丢弃超出部分。若小于参数 length,调用将在文件尾部添加一系列空字节或一个文件空洞。
truncate() 调用通过路径名指定文件。
ftruncate() 通过文件描述符指定文件。
四、非阻塞I/O
在打开文件时指定 O_NONBLOCK 标志,目的有二:
- 若 open() 调用未能立即打开文件,则返回错误,而非陷入阻塞。
- 调用 open() 成功后,后续的 I/O 操作也是非阻塞的。若 I/O 系统调用未能立即完成,则可能会只传输部分数据,或者系统调用失败,并返回 EAGAIN 或 EWOULDBLOCK 错误。
管道、FIFO、套接字、设备(比如终端、伪终端)都支持非阻塞模式。
因为无法通过 open() 来获取管道和套接字的文件描述符,所以要启用非阻塞标志,就必须使用 fcntl() 的 F_SETFL 命令。
五、大文件IO
六、/dev/fd目录
对于每个进程进程,内核都提供有一个特殊的虚拟目录 /dev/fd。该目录中包含 “/dev/fd/n” 形式的文件名,其中 n 是与进程中的打开文件描述符相对应的编号。
/dev/fd 实际上是一个符号链接,链接到 Linux 所专有的 /proc/self/fd 目录。
七、创建临时文件
有些程序需要创建一些临时文件,仅供其在运行期间使用,程序终止后立即删除。
1、mkstemp
#include <stdlib.h>
int mkstemp(char *template);
int mkostemp(char *template, int flags);
int mkstemps(char *template, int suffixlen);
int mkostemps(char *template, int suffixlen, int flags);
mkstemp() 函数生成一个唯一文件名并打开该文件,返回一个可用于 IO 调用的文件描述符。
2、tmpfile
#include <stdio.h>
FILE *tmpfile(void);
tmpfile() 执行成功,将返回一个文件流供 stdio 库函数使用。文件关闭后立即删除临时文件。