3.2 文件描述符
1、对于内核而言,所有打开的文件都通过文件描述符引用。文件描述符是一个非负整数。
(1)文件描述符是一个非负整数。
2、UNIX系统shell把文件描述符0(STDIN_FILENO)与进程的标准输入关联,文件描述符1(STDOUT_FILENO)与标准输出关联,文件描述符2(STDERR_FILENO)与标准错误关联。这些常量定义在头文件<unisted.h>
中定义。
3、文件描述符的变化范围是0~OPEN_MAX-1。
3.3 函数open和openat
1、调用open或openat可以打开或创建一个文件:
#include <fcntl.h>
int open(const char *path, int oflag,... /* mode_t mode */);
int openat(int fd, const char *path, int oflag, ... /* mode_t */);
两函数的返回值:若成功,返回文件描述符;若出错,返回-1。
(1)path参数是要打开或创建文件的名字。
(2)oflag参数用来说明此函数的多个选项(这些常量在头文件<fcnt.h>
中定义)
O_RDONLY 只读打开。
O_WRONLY 只写打开。
O_RDWR 读、写打开。
O_EXEC 只执行打开。
O_SEARCH 只搜索打开(应用于目录)
O_APPEND 每次写时都追加到文件的尾端。
O_CLOEXEC 把FD_CLOEXEC常量设置为文件描述符标志。
O_CREAT 若此文件不存在这创建它。使用此选项时,open
函数需同时说明第三个参数mode(openat函数需
要说明第四个参数mode),用mode指定该新文件
的方位权限位。
O_DIRECTORY 如果path引用的不是目录,则出错
O_EXCL 如果同时指定了O_CREAT,而文件已经存在,则出
错。用此可以测试一个文件是否存在,如果不存在
,则创建此文件,这使测试和创建两者成为一个原
子操作。
O_NOCTTY 如果path引用的是终端设备,则不将该设备分配
作为进程的控制终端。
O_NOFOLLOW 如果path引用的是一个符号链接,则出错。
O_NONBLOCK 如果path引用的是一个FIFO、一个块特殊文件或
一个字符特殊文件,则此选项为文件的本次打开操
作和后续的I/O操作设置非阻塞方式。
O_SYNC 使每次write等待物理I/O操作完成,包括由该
write操作引起的文件的属性更新所需的I/O。
O_TRUNC 如果此文件存在,而且为只读-写成功打开,则将
其长度截断为0。
O_TTY_INIT 如果打开一个未打开的终端设备,设置非标准
termios,使其符合Single Unix
Speciflacation。
O_DSYNC 使每次write要等待物理I/O操作完成,但是如果
该写操作并不影响读取刚写入的数据,则不需要等
待文件属性被更新。
O_RSYNC 使每一个文件描述符作为参数进行的read操作等待,
直至所有对文件同一部分挂起的写操作都完成。
2、由open和openat函数返回的文件描述符一定是最小的未用描述符数值。
3、fd参数把open和openat函数区分开,共有3种可能性:
(1)path参数指定的是绝对路径名,在这种情况下,fd参数被忽略,openat函数就相当于open函数。
(2)path参数指定的是相对路径名,fd参数指出了相对路径名在系统开始的地址。fd参数是通过打开相对路径名所在的目录来获取。
(3)path参数指定了相对路径名,fd参数具有特殊值AT_FDCWD。在这种情况下,路径名在当前工作目录中获取,openat函数在操作上与open函数类似。
3.4 函数creat
1、可以调用creat函数创建一个新文件:
#include <fcntl.h>
int creat(const char *path, mode_t mode);
返回值:若成功,返回为只写打开的文件描述符;若出错,返回-1
(1)此函数等效于:
open(path,O_WRONLY | O_TRUNC, mode);
(2)creat的一个不足之处是它以只写方式打开所创建的文件。
3.5 函数close
1、可调用close函数关闭一个文件:
#include <unistd.h>
int close (int fd);
返回值:若成功,返回0,若出错,返回-1
(1)关闭一个文件时还会释放该进程加在该文件上的所有记录锁。
(2)当一个进程终止时,内核自动关闭它所有的打开文件。
3.6 函数lseek
1、每个打开文件都有一个与其关联的”当前文件偏移量(current file offset)。它通常是一个非负整数,用以度量从文件开始处计算的字节数”
2、按系统默认的情况,当打开一个文件时,除非指定O_APPEND选项,否则偏移量被设置为0。
3、可以调用lseek显式地为一个打开文件设置偏移量。
#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);
返回值:若成功,返回新的文件偏移量;若出错,返回-1
2、对offset的解释与参数whence的值有关:
(1)若whence是SEEK_SET,则将该文件的偏移量设置为距文件开始处的offset个字节。
(2)若whence是SEEK_CUR,则将该文件的偏移量设置为当前值加offset,offset可为正或负。
(3)若whence是SEEK_END,则将该文件的偏移量设置为文件长度加offset,offset可正可负。
3、若lseek成功执行,则返回新的文件偏移量,为此可以用下列方式确定打开文件的当前偏移量:
off_t currpos;
currpos = lseek(fd, 0, SEEK_CUR);
这种方法也可以确定所涉及的文件是否可以设置偏移量。如果文件描述符是指向一个管道、FIFO或网络套接字,则lseek返回-1,并将errno设置为ESPIPE。
4、实例3-1:测试对其标准输入能否设置偏移量:
/*************************************************************************
> File Name: instance3-1.c
> Author: King
> Mail: arturiapendragon_1@163.com
> Created Time: 2017年05月04日 星期四 10时16分37秒
************************************************************************/
/*
* 测试标准输入能否设置偏移量
*/
#include "apue.h"
int main(void)
{
if (lseek(STDIN_FILENO, 0, SEEK_CUR) == -1)
printf("cannot seek\n");
else
printf("seek OK\n");
exit(0);
}
5、lseek仅将当前的文件偏移量记录在内核中,它并不引起任何I/O操作。
6、文件偏移量可以大于文件的当前长度,在这种情况下,对该文件的下一次写将加长该文件,并在文件中构成一个空洞,这一点是允许的。位于文件中但没有写过的字节都被读为0。
7、文件中的空洞并不要求在磁盘上占用存储区。
8、实例3-2 创建一个具有空洞的文件。
/*************************************************************************
> File Name: instance3-2.c
> Author: King
> Mail: arturiapendragon_1@163.com
> Created Time: 2017年05月04日 星期四 10时22分57秒
************************************************************************/
/* 创建一个具有空洞的文件 */
#include "apue.h"
#include <fcntl.h>
char buf1[] = "abcdefghij";
char buf2[] = "ABCDEFGHIJ";
int main(void)
{
int fd;
if ((fd=creat("file.hole",FILE_MODE))<0)
err_sys("creat error");
if (write(fd,buf1,10) != 10)
err_sys("buf1 write error");
/* offset now = 10 */
if (lseek(fd,16384,SEEK_SET) == -1)
err_sys("lseek error");
/* offset now 16384 */
if (write(fd,buf2,10) != 10)
err_sys("buf2 write error");
/* offset now 16394 */
exit(0);
}
3.7 函数read
1、调用函数read从打开文件中读数据:
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t nbytes);
返回值:读到的字节数,若已到文件结尾,返回0,若出错,返回-1
2、有多种情况可以使实际读到的字节数少于要求读的字节数:
(1)读普通文件时,在读取要求字节数之前已经到达了文件尾端。
(2)当从终端设备时,通常一次最多读取一行
(3)当从网络读取时,通网络中的缓冲机制可能造成返回值小于所要求读的字节数。
(4)当从管道或FIFO读时,如若管道包含的字节少于所需的数量,那么read将只返回实际可用的字节数。
(5)当从某些面向记录的设备(如磁带)读时,一次最多返回一个记录。
(6)当一信号造成中断,而已经读了部分数据量时。
3.8 函数write
1、调用write函数向打开文件写数据:
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t nbytes);
返回值:若成功,返回已写的字节数;若出错,返回-1
2、其返回值与参数nbytes的值相同,否则表示出错。write出错的一个原因是磁盘写满,或者超过了一个给定进程的文件长度限制。
3.9 I/O的效率
1、实例3-5:使用read和write函数复制一个文件:
#include "apue.h"
#define BUFFSIZE 4096
int main(void)
{
int n;
char buf[BUFFISZE];
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);
}
3.10 文件共享
1、内核使用3种数据结构表示打开文件,它们之间的关系决定了在文件共享方面一个进程对另一个进程的影响。
(1)每个进程在进程中都有一个记录项,记录项中包含一张文件描述符表,可将其视为一个矢量,每个描述符占用一项,与每个文件描述符的关联是:
a. 文件描述符标志
b. 指向一个文件表项的指针
(2)内核为所有打开文件维持一张文件表,每个文件表项包含:
a. 文件状态标志(读、写、添写、同步和非阻塞等)
b. 当前文件偏移量
c. 指向该文件v节点表项的指针
(3)每个文件(或设备)都有一个v节点(v-node)结构。v节点包含了文件类型相对此文件进行各种操作函数的指针。
3、下图显示一个进程的3张表的关系。该进程有两个不同的打开文件。一个从标准输入打开,一个从标准输出打开。
4、两个独立进程各自打开了同一个文件,关系图如下:
(1)之所以每个进程都获得自己的文件表项,是因为这可以使每个进程都有它自己的对该文件的当前偏移量。
5、lseek函数只修改文件表项中的当前文件偏移量,不进行任何I/O操作。
6、文件描述标志用于一个进程的描述符,文件状态标志用于指向该给定文件表项的任何进程中的所有描述符。
3.11 原子操作
1、任何要求多于一个函数的操作都不是原子操作,因为在两个函数调用之间,内核有可能会临时挂起进程。
2、函数pread和pwrite
#include <unistd.h>
ssize_t pread(int fd, void *buf, size_t nbytes,
off_t offset);
返回值:读到的字节数,若已到文件尾,返回0;若出错,返回-1
size_t pwrite(int fd, void *buf, size_t nbytes,
off_t offset);
返回值:若成功,返回已写的字节数;若出错,返回-1
3、调用pread相当于调用lseek后调用read,但是pread又与这种顺序调用有下列重要区别:
(1)调用pread时,无法中断其定位和读操作。
(2)不更新当前文件偏移量。
4、一般而言,原子操作(atomic operation)指的是由多步组成的一个操作。如果该操作原子地执行,则要么执行完所有步骤,要么一步也不执行,不可能只执行所有步骤的一个子集。
3.12 函数dup和dup2
1、下面两个函数可用来复制一个现有的文件描述符:
#include <unistd.h>
int dup(int fd);
int dup2(int fd, int fd2);
两个函数的返回值:若成功,返回新的文件描述符;若出错,返回-1
2、由dup返回的新的文件描述符一定是当前可用文件描述符中的最小数值。
3、对于dup2,可以用fd2参数指定新描述符的值:
(1)如果fd2已经打开,则先将其关闭。
(2)如若fd等于fd2,则dup2返回fd2,而不关闭它。
(3)否则fd2的FD_CLOEXEC文件描述符标志就被清楚,这样fd2在进程调用exec时是打开状态。
4、这些函数返回的新文件描述符与参数fd共享同一个文件表项:
5、dup2并不完全等同于close加上fcntl。他们的区别如下:
(1)dup2是一个原子操作,而close和fcntl包括两个函数调用。有可能在close和fcntl之间调用了信号捕获函数,它可能修改文件描述符。如果不同的线程改变了文件描述符的话也会出现相同的问题。
(2)dup2和fcntl有一些不同的errno
3.13 函数sync、fsync和fdatasync
1、通常,当内核需要重用缓冲区来存放其他磁盘快数据时,它会把所有延迟写数据块写入磁盘。为了保证磁盘上实际文件系统与缓冲区中内容的一致性,UNIX系统提供了sync、fsync、和fdatasync三个函数:
#include <unistd.h>
int fsync(int fd);
int fdatasync(int fd);
返回值:若成功,返回0,若出错,返回-1
void sync(void);
2、sync只是将所有修改过的块缓冲区排入写队列,然后就返回,它并不等待实际写磁盘操作结束。
3、fsync函数只对由文件描述符fd指定的一个文件起作用,并且等待写入磁盘结束才返回。fync可用于数据库这样的应用程序,这种应用程序需要确保修改过的块立即写到磁盘上。
4、fdatasnyc函数类似于fsync,但它只影响文件的数据部分,而除数据外,fsync还会同步更新文件的属性。
3.14 函数 fcntl
1、fcntl 函数可以改变已经打开文件的属性:
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* int arg */);
返回值:若成功,则依赖于cmd;若错误,返回-1。
2、fcntl函数有一下5种功能:
(1)复制一个已有的描述符(cmd =F_DUPFD或 F_DUPFD_CLOEXEC)
(2)获取/设置文件描述符标志(cmd = F_GETFD 或 F_SETFD)
(3)获取/设置文件状态标志(cmd=F_GETFL或F_SETFL)
(4)获取/设置异步I/O所有权(cmd=F_GETOWN或F_SETOWN)
(5)获取/设置记录锁(cmd=F_GETLK、F_SETLKW)
8种cmd:
1. F_DUPFD:复制文件描述符fd。新文件描述符作为函数值返回。它是尚未打开的各描述符中大雨或等于第3个参数值(取为整型值)中各值的最小值。
2. F_DUPFD_CLOEXEC:复制文件描述符,设置与新描述符关联的FD_CLOEXEC文件描述符标志的值,返回新文件描述符。
3. FD_GETFD:对应于fd的文件描述符标志作为函数返回。当前只定义了一个文件描述符标志FD_CLOEXEC。
4. FD_SETFD:对应于fd设置文件描述符标志。新标志值按第3个参数(取为整型值)设置。
5. F_GETFL:对应于fd的文件状态标志作为函数值返回。
O_RDONLY 只读打开
O_WRONLY 只写打开
O_RDWR 读、写打开
O_EXEC 只执行打开
O_SEARCH 只搜索打开目录
O_APPEND 追加写
O_NONBLOCK 非阻塞模式
O_SYNC 等待写完成(数据和属性)
O_DSYNC 等待写完成(仅数据)
O_RSYNC 同步读和写
O_FSYNC 等待写完成(仅FreeBD和Mac OS X)
O_ASYNC 异步I/O(仅FreeBD和Mac OS X)
6. F_SETFL:将文件状态标志设置为第3个参数的值(取为整型值)。可以更改的几个标志是:O_APPEND、O_NONBLOCK、O_SYNC、O_DSYNC、O_RSYNC、O_FSYNC和O_ASYNC
7. F_GETOWN:获取当前接收SIGIO和SIGURG信号和进程ID或进程组ID。
8. F_SETOWN:设置接收SIGIO和SIGURG信号的进程ID和进程组ID。正的arg指定一个进程ID,负的arg表示等于arg绝对值的一个进程组ID。
3、实例3-11:程序第一个参数指定文件描述符,并对于该描述符打印所选择的文件标志说明:
#include "apue.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("write only");
break;
case O_RDWR:
printf("read write");
break;
default:
err_dump("unkown access mode");
}
if(val & O_APPEND)
printf(", append");
if(val & NONBLOCK)
printff(", 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);
}
3.15 函数ioctl
1、ioctl 函数一直都是I/O操作的杂物箱,终端I/O是使用ioctl最多的地方。
#include <unistd.h> /* System V */
#include <sys/ioctl.h> /* BSD and Linux */
int ioctl(int fd, int request, ...);
返回值:若出错,返回-1;若成功,返回其他值。
3.16 /dev/fd
1、较新的系统都提供名为/dev/fd 的目录,其目录项是名为0、1、2等的文件。打开 /dev/fd/n 等效于复制描述符n(假定描述符n是打开的)。
2、用open打开/dev/fd/0,相当于描述符0和fd共享同一文件表项,所以无论打开mode如何,并不会改变文件表项中文件状态标志。但不返回错误。
文件描述符0已经打开,并且为只读模式
fd=("/dev/fd/0",O_RDWR);
仍不能对fd进行写操作
3、Linux实现中的/dev/fd是个例外。它把文件描述符映射成指向底层物理文件的符号链接。因此返回新文件描述符(符号链接所指向的文件的文件描述符)的模式与/dev/fd文件描述符的模式并不相关(符号链接的文件描述符)
4、某些系统提供路径名/dev/stdin、/dev/stdout、/dev/stderr,这些等效于/dev/fd0、/dev/fd1、/dev/fd2。
5、/dev/fd 文件主要由shell使用,它允许使用路径名作为调用参数的程序,能用处理其他路径名的相同方式处理标准输入和输出。如:
cat(1)命令对其命令行参数采取了特殊处理,将单独的一个字符"-"解释为标准输入:
filter file2 | cat file1 - file3 | lpr
等价于
filter file 2 | cat file1 /dev/fd0 file3 | lpr
cat 先读file1,再读file2,最后读file3