原子操作:一个独立而不可分隔的操作。
所有系统调用都是以原子操作方式执行的。
原子操作规避了竞争状态。
竞争状态:操作共享资源的两个进程(或线程),其结果取决于一个无法预测的顺序,即这些进程获得CPU使用权的先后相对顺序。
思考以下程序:
#include
#include
#include
#include
#include
#include
#include
#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;
}
分析:
两个进程对同一个文件同时操作,产生竞争状态,使一个进程得到错误的结论。
在使用 open() 系统调用时,同时指定 O_EXCL 和 O_CREAT 作为 open() 的标志位,保证进程是打开文件的创建者。对文件是否存在的检查和创建文件属于同一原子操作。
要理解文件描述符和打开文件之间的关系,需要查看内核维护的3个数据结构:
1、进程级的文件描述符表。
2、系统级的打开文件表。
3、文件系统的 i-node 表。
每一条目录都记录了单个文件描述符的相关信息。
打开文件表中各条目称为打开文件句柄。一个打开文件句柄存储了与一个打开文件相关的全部信息。
i-node 在磁盘和内存中有所差异。
操纵文件描述符.
#include
#include
int fcntl(int fd, int cmd, ... /* arg */ );
1、fd
open() 系统调用返回的文件描述符。
2、cmd
命令 | 含义 |
---|---|
F_GETFL | 获取文件状态标志 |
F_SETFL | 修改打开文件的某些状态标志 |
F_DUPFD | 复制文件描述符 |
…… | …… |
access_mode = flags & O_ACCMODE;
if(access_mode == O_WRONLY || access_mode == RDWR)
printf("file is writable\n");
newfd = fcntl(oldfd,F_DUPFD,startfd);
该系统调用为 oldfd 创建一个副本,且将使用大于等于 startfd 的最小未用值作为描述符编号。总是能将 dup() 和 dup2() 调用改写为对 close() 和 fcntl() 的调用。
#include
int dup(int oldfd);
int dup2(int oldfd, int newfd);
#define _GNU_SOURCE /* See feature_test_macros(7) */
#include /* Obtain O_* constant definitions */
#include
int dup3(int oldfd, int newfd, int flags)
dup() 调用复制一个打开的文件描述符 oldfd,并返回一个新文件描述符,二者都指向同一个打开的文件句柄。
dup2() 调用会为 oldfd 参数所指定的文件描述符创建副本,其编号由 newfd 参数指定。如果 newfd 参数指定文件描述符已经打开,那么 dup2() 会先将其关闭。dup2() 会忽略 newfd 关闭期间出现的任何错误。
dup3() 系统调用完成的工作与 dup2() 相同,只是增加了一个附加参数 flag,这是一个可以修改系统调用行为的位掩码。
#include
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 操作,且不会其他线程修改文件偏移量而受到影响。
struct iovec{
void *iov_base;
size_t iov_len;
};
#include
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);
readv() 系统调用实现分散输入功能:从文件描述符 fd 所指代的文件中读取一片连续的字节,然后将其散置(分散放置)于 iov 指定的缓冲区中。这一散置动作从 iov[0] 开始,依次填满每个缓冲区。
原子性是 readv() 的重要属性。
writev() 系统调用实现集中输出:将 iov 所指定的所有缓冲区中的数据拼接(“集中”)起来,然后以连续的字节序列写入文件描述符 fd 所指代的文件中。对缓冲区中数据 “集中” 始于 iov[0] 所指代的缓冲区,并按数组顺序展开。
原子性是 writev() 的重要属性。
readv() 调用和 writev() 调用的主要优势在于便捷。如下两种方案,任选其一都可替代对 writev() 的调用。
preadv() 和 pwriev() 系统调用所执行的任务于 readv() 和 writev() 相同,但执行 I/O 的位置将由 offset 参数指定(类似 pread() 和 pwrite())。
#include
#include
int truncate(const char *path, off_t length);
int ftruncate(int fd, off_t length);
truncate() 系统调用和 ftruncate() 系统调用将文件大小设置为 length 参数指定的值。
若文件当前长度大于参数 length,调用将丢弃超出部分。若小于参数 length,调用将在文件尾部添加一系列空字节或一个文件空洞。
truncate() 调用通过路径名指定文件。
ftruncate() 通过文件描述符指定文件。
在打开文件时指定 O_NONBLOCK 标志,目的有二:
管道、FIFO、套接字、设备(比如终端、伪终端)都支持非阻塞模式。
因为无法通过 open() 来获取管道和套接字的文件描述符,所以要启用非阻塞标志,就必须使用 fcntl() 的 F_SETFL 命令。
对于每个进程进程,内核都提供有一个特殊的虚拟目录 /dev/fd。该目录中包含 “/dev/fd/n” 形式的文件名,其中 n 是与进程中的打开文件描述符相对应的编号。
/dev/fd 实际上是一个符号链接,链接到 Linux 所专有的 /proc/self/fd 目录。
有些程序需要创建一些临时文件,仅供其在运行期间使用,程序终止后立即删除。
#include
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 调用的文件描述符。
#include
FILE *tmpfile(void);
tmpfile() 执行成功,将返回一个文件流供 stdio 库函数使用。文件关闭后立即删除临时文件。