1.1 文件描述符
文件描述符(fd)相当于windows编程中的文件句柄,使一个非负整数,引用一个打开的文件。
Unix的惯例是文件描述符0(STDIN_FILENO)是标准输出,1(STDOUT_FILENO)是标准输出,2(STDERR_FILENO)是标准错误输出。
1.2 文件的打开与关闭
1.2.1 相关函数
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
int creat(const char *pathname, mode_t mode);
int close(int fd);
1.2.2 flag的选项
O_RDONLY、O_WRONLY和O_RDWR,这三个标志中只能指定一个。另外还有以下常数可以选择:O_APPEND、O_CREAT、O_EXEL(与O_CREAT同时指定,而文件已存在则出错,否则创建)、O_TRUNC、O_NONBLOCK(非阻塞方式)、O_SYNC(同步写,每次write都等到物理I/O完成)等常用选项。
1.2.3 创建新文件
当flags指定O_CREAT时,需要指定第三个参数mode,说明新文件的存取权限。也可以用creat函数,creat等价于:
open(pathname, O_WRONLY | O_CREAT | O_TRUNC, mode)
1.3 改变文件的偏移量
1.3.1 相关函数
off_t lseek(int fildes, off_t offset, int whence);
1.3.2 设置文件位移量
参数offset指定偏移量,参数whence可指定为SEEK_SET(相对于文件头)、SEEK_CUR(相对于当前位置)、SEEK_END(相对于从文件尾)。
因为位移量可能是负值,比较lseek的返回值时必须谨慎,不要测试它是否小于0,而要测试它是否等于-1。
1.3.3 文件空洞
文件的位移量可以大于文件当前长度,这种情况下,对该文件的下一次写操作将延长该文件。形成一个空洞,空洞中的字节都为0。
1.4 文件的读写
1.4.1 相关函数
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
1.4.2 读文件
read函数的返回值是实际读到的字节数,他可能少于要求读的字节数count:读到文件尾了;或者读的是终端设备、网络设备或者某些面向记录的设备时。
1.4.3 写文件
写操作从当前位移开始。若打开文件时设置了O_APPEND选项,则每次写之前,都会将当前位移量设为文件尾。
1.4.4 效率
使用不同的BUFFSIZE将数据从标准输入读入然后写到标准输出。在下面的测试中,标准输入重定向到一个7175K的文件,标准输出重定向到/dev/null。
BUFFSIZE为1:
real 0m15.945s
user 0m5.420s
sys 0m10.500s
BUFFSIZE为4:
real 0m4.025s
user 0m1.570s
sys 0m2.460s
BUFFSIZE为16:
real 0m1.057s
user 0m0.200s
sys 0m0.860s
BUFFSIZE为256:
real 0m0.153s
user 0m0.030s
sys 0m0.120s
BUFFSIZE为1024:
real 0m0.093s
user 0m0.000s
sys 0m0.100s
BUFFSIZE为4096:
real 0m0.081s
user 0m0.010s
sys 0m0.080s
BUFFSIZE为16384:
real 0m0.078s
user 0m0.000s
sys 0m0.080s
BUFFSIZE为65536:
real 0m0.080s
user 0m0.000s
sys 0m0.080s
继续增大BUFFSIZE对系统时间并没有影响。
1.5 复制文件描述符
1.5.1 相关函数
int dup(int oldfd);
int dup2(int oldfd, int newfd);
1.5.2 说明
这些函数返回一个和oldfd共享同一文件表项的新fd。不同之处是dup2可以指定新fd的值,并且如果newfd已经打开,则先将其关闭。他们可以用fcntl(oldfd,F_DUPFD, 0/newfd)实现,不同的只是errno,并且dup2是一个原子操作。
1.6 fcntl函数
1.6.1 相关函数
int fcntl(int fd, int cmd);
int fcntl(int fd, int cmd, long arg);
int fcntl(int fd, int cmd, struct flock *lock);
1.6.2 功能
- 复制一个现存的描述符(cmd=F_DUPFD)。
- 获得/设置文件描述符标志(cmd = F_GETFD或F_SETFD)。
- 获得/设置文件状态标志(cmd d = F_GETFL或F_SETFL)。
- 获得/设置异步I / O有权(cmd = F_GETOWN或F_SETOWN)。
- 获得/设置记录锁(cmd = F_GETLK, F_SETLK或F_SETLKW)。
1.6.3 F_GETFL或F_SETFL
对于这两种操作,可以获得或者设置文件的状态标志。F_GETFL时可以获得O_RDONLY、O_WRONLY、O_RDWR、O_APPEND、O_NONBLOCK、O_SYNC和O_ASYNC。对于前三种标志,必须使用屏蔽字O_ACCMODE,然后将结果与这三种标志一一比较。F_SETFL时只能设置后四种标志。
accmode = val & O_ACCMODE;
if (accmode == O_RDONLY) …
else if (accmode == O_WRONLY) …
else if (accmode == O_RDWR) …
else err_dump("unknown access mode");
if (val & O_APPEND) …
if (val & O_NONBLOCK) …
……
1.7 记录锁
1.7.1 相关函数:
int fcntl(int fd, int cmd);
int fcntl(int fd, int cmd, struct flock *lock);
1.7.2 相关结构:
struct flock {
short l_type; /* F_RDLCK, F_WRLCK, F_UNLCK */
short l_whence; /* SEEK_SET, SEEK_CUR, SEEK_END */
off_t l_start; /* Starting offset for lock */
off_t l_len; /* Number of bytes to lock */
pid_t l_pid; /* PID of process blocking our lock */
};
l_start和l_whence决定加锁或解锁区域的起始位置,l_len决定其长度。该区域可以越过文件尾端,但不能越过文件起始位置。l_len为0,则表示锁的区域从开始位置直至文件尾,而不管文件如何增长。锁整个文件的通常方法是:l_start为0,l_whence为SEEK_SET,l_len为0。
1.7.3 共享锁和独占锁
共享锁,又叫读锁(RDLCK);独占锁,又叫写锁(WRLCK)。
1.7.1 相关函数:
int fcntl(int fd, int cmd);
int fcntl(int fd, int cmd, struct flock *lock);
1.7.2 相关结构:
struct flock {
short l_type; /* F_RDLCK, F_WRLCK, F_UNLCK */
short l_whence; /* SEEK_SET, SEEK_CUR, SEEK_END */
off_t l_start; /* Starting offset for lock */
off_t l_len; /* Number of bytes to lock */
pid_t l_pid; /* PID of process blocking our lock */
};
1.7.3 共享锁和独占锁
共享锁,又叫读锁(RDLCK);独占锁,又叫写锁(WRLCK)。
表格 1 (读锁和写锁)
当前所有/要求 |
读锁 |
写锁 |
读锁(一把或多把) |
可以 |
拒绝 |
写锁 |
拒绝 |
拒绝 |
1.7.4 加锁和解锁
根据fcntl函数的cmd参数进行不同操作:
-
F_GETLK:检查是否可以建立参数lock描述的锁
- F_SETLK:设置lock描述的锁,出错立刻返回
- F_SETLKW:F_SETLK的阻塞版,若不能加锁则等待
1.7.5 锁的继承和释放
进程终止时,它建立的锁全部释放。
关闭一个文件描述符时,则与该描述符相关的文件上的所有由这个进程设置的锁都被释放,即使进程还有打开的描述符指向该文件。
fork产生的子进程不继承父进程设置的锁。而exec调用后,新程序可以继承原执行程序的锁。
1.7.6 建议性锁和强制性锁
建议性锁(Advisory lock)并不能保证其他对文件有写权限的进程读写文件,使用建议性锁的程序必须以一致的方法处理记录锁。使用强制性锁(Mandatory lock),则内核对每个open、read和write都要检查调用进程是否违背了某一把锁的作用。
要使用强制性锁,首先文件所在的文件系统必须开启此功能(mount的时候加上-o mand参数);然后打开其set-guid,关闭其组执行位(chmod命令中的g-x和g+s)即可。强制性锁的使用和建议性锁基本相同。]
强制性锁不是POSIX标准。
1.8 I/O多路转接
1.8.1 相关函数:
int select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
int pselect(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, const struct timespec *timeout, const sigset_t *sigmask)
int poll(struct pollfd *ufds, unsigned int nfds, int timeout);
FD_CLR(int fd, fd_set *set);
FD_ISSET(int fd, fd_set *set);
FD_SET(int fd, fd_set *set);
FD_ZERO(fd_set *set);
1.8.2 相关结构
fd_set是一个fd的集合,有四个宏可对其进行操作:清空一个fd即(FD_ZERO),检测指定 fd是否在集中(FD_ISSET),加入(FD_SET)和移除(FD_CLR)一个fd。
select使用结构timeval:
struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
pselect使用结构timespec:
struct timespec {
long tv_sec; /* seconds */
long tv_nsec; /* nanoseconds */
};
1.8.3 说明
select等待指定的fd对应的文件准备就绪。对于readfds中的文件,等待起可读;对于writefds中的文件,等待其可写;对于exceptfds中的文件等待其发生异常。而n则是参数中最大的fd加1,可以设为FD_SETSIZE,但会影响效率。timeout则是等待的时间,为NULL则无限等待。返回值是就绪的fd的个数。
在select函数中,如果在一个fd上碰到了文件结束,则select认为该fd是可读的,而不是指示一个异常。
pselect和select类似,只是使用了timespec结构指定时间,另外和sigsuspend类似,在等待时会将信号屏蔽改为sigmask。
poll是select函数的另一版本,不同之处是它构造一格pollfd结构数组,对每个元素指定一个fd及对其关心的条件。
1.8.4 使用
select可以作为提供更精确的sleep函数使用。
当必须读写多个fd时,使用select函数,可以避免长时间阻塞在一个fd上而另一个fd的数据却得不到及时处理的情况。
1.9 读写多个缓存
1.9.1 相关函数
ssize_t readv(int fd, const struct iovec *vector, int count);
ssize_t writev(int fd, const struct iovec *vector, int count);
1.9.2 相关结构
struct iovec {
void *iov_base; /* Starting address */
size_t iov_len; /* Number of bytes */
};
1.9.3 说明
这两个函数用于读写多个缓存。读写的顺序是按数组的顺序。
1.9.4 性能
两个缓存,一个512,一个1024,分别用三种方法将其写入文件10000遍,第一种是调用writev,第二种是每个循环调用两次write,第三种是声名一个1536的缓存,将前两个缓存的内容都拷贝入这个缓存中然后调用一次write,结果如下。右边是apue上的测试结果,他的缓存大小为100、200,机器是80386。
one writev:
real 0m0.318s 8.2s
user 0m0.000s 0.3s
sys 0m0.320s 7.8s
two write:
real 0m0.990s 13.7s
user 0m0.000s 0.5s
sys 0m0.330s 13.1s
memcpy and one write
real 0m0.255s 8.1s
user 0m0.010s 0.7s
sys 0m0.250s 7.3s
很奇怪,调用2次write所用系统时间应该是调用一次write或writev的2倍,但我的出的结果却是调用一次writev和两次write所用系统时间差不多,而memcpy后调用一次write反而比一次writev还快。
1.10 I/O存储映射
1.10.1 概念
存储映射使一个磁盘文件或设备同存储空间的一个缓存区相映射。于是从缓存中存取数据就相当于文件读写。
1.10.2 相关函数
void * mmap(void *start, size_t length, int prot , int flags, int fd, off_t offset);
int munmap(void *start, size_t length);
int msync(void *start, size_t length, int flags);
1.10.3 说明
start参数一般设为0,即让系统选择映射缓存取的起始地址,这个地址通过返回值获得。fd,length和offset指定文件区域。prot指定对映射区的保护方式,flags则是一些标识位。MAP_SHARED表示和其他进程共享此映射,存取缓存取回直接反映到文件中;MAP_PRIVATE表示独享此进程,写入映射缓存区并不影响原文件。这两个标识只能指定一个。
munmap解除存储映射。要注意的是,关闭文件描述符并不解除对此文件的映射。调用munmap也并不使映射区的内容写到文件中,同步文件和映射区可以调用msync函数。
1.10.4 使用
使用mmap/memcpy进行文件操作比read/write快,因为前者直接对映射取缓存进行操作,而对于后者,内核需要将数据在用户缓存和它自己的缓存之间进行拷贝。
但是内存映射不能用在某些设备(如网络和终端设备)之间进行复制,并且操作时必须注意文件的长度是否改变。
1.8.1 相关函数:
int select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
int pselect(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, const struct timespec *timeout, const sigset_t *sigmask)
int poll(struct pollfd *ufds, unsigned int nfds, int timeout);
FD_CLR(int fd, fd_set *set);
FD_ISSET(int fd, fd_set *set);
FD_SET(int fd, fd_set *set);
FD_ZERO(fd_set *set);
1.8.2 相关结构
fd_set是一个fd的集合,有四个宏可对其进行操作:清空一个fd即(FD_ZERO),检测指定 fd是否在集中(FD_ISSET),加入(FD_SET)和移除(FD_CLR)一个fd。
select使用结构timeval:
struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
pselect使用结构timespec:
struct timespec {
long tv_sec; /* seconds */
long tv_nsec; /* nanoseconds */
};
1.8.3 说明
select等待指定的fd对应的文件准备就绪。对于readfds中的文件,等待起可读;对于writefds中的文件,等待其可写;对于exceptfds中的文件等待其发生异常。而n则是参数中最大的fd加1,可以设为FD_SETSIZE,但会影响效率。timeout则是等待的时间,为NULL则无限等待。返回值是就绪的fd的个数。
在select函数中,如果在一个fd上碰到了文件结束,则select认为该fd是可读的,而不是指示一个异常。
pselect和select类似,只是使用了timespec结构指定时间,另外和sigsuspend类似,在等待时会将信号屏蔽改为sigmask。
poll是select函数的另一版本,不同之处是它构造一格pollfd结构数组,对每个元素指定一个fd及对其关心的条件。
1.8.4 使用
select可以作为提供更精确的sleep函数使用。
当必须读写多个fd时,使用select函数,可以避免长时间阻塞在一个fd上而另一个fd的数据却得不到及时处理的情况。
1.9.1 相关函数
ssize_t readv(int fd, const struct iovec *vector, int count);
ssize_t writev(int fd, const struct iovec *vector, int count);
1.9.2 相关结构
struct iovec {
void *iov_base; /* Starting address */
size_t iov_len; /* Number of bytes */
};
1.9.3 说明
这两个函数用于读写多个缓存。读写的顺序是按数组的顺序。
1.9.4 性能
两个缓存,一个512,一个1024,分别用三种方法将其写入文件10000遍,第一种是调用writev,第二种是每个循环调用两次write,第三种是声名一个1536的缓存,将前两个缓存的内容都拷贝入这个缓存中然后调用一次write,结果如下。右边是apue上的测试结果,他的缓存大小为100、200,机器是80386。
one writev:
real 0m0.318s 8.2s
user 0m0.000s 0.3s
sys 0m0.320s 7.8s
two write:
real 0m0.990s 13.7s
user 0m0.000s 0.5s
sys 0m0.330s 13.1s
memcpy and one write
real 0m0.255s 8.1s
user 0m0.010s 0.7s
sys 0m0.250s 7.3s
1.10.1 概念
存储映射使一个磁盘文件或设备同存储空间的一个缓存区相映射。于是从缓存中存取数据就相当于文件读写。
1.10.2 相关函数
void * mmap(void *start, size_t length, int prot , int flags, int fd, off_t offset);
int munmap(void *start, size_t length);
int msync(void *start, size_t length, int flags);
1.10.3 说明
start参数一般设为0,即让系统选择映射缓存取的起始地址,这个地址通过返回值获得。fd,length和offset指定文件区域。prot指定对映射区的保护方式,flags则是一些标识位。MAP_SHARED表示和其他进程共享此映射,存取缓存取回直接反映到文件中;MAP_PRIVATE表示独享此进程,写入映射缓存区并不影响原文件。这两个标识只能指定一个。
munmap解除存储映射。要注意的是,关闭文件描述符并不解除对此文件的映射。调用munmap也并不使映射区的内容写到文件中,同步文件和映射区可以调用msync函数。
1.10.4 使用
使用mmap/memcpy进行文件操作比read/write快,因为前者直接对映射取缓存进行操作,而对于后者,内核需要将数据在用户缓存和它自己的缓存之间进行拷贝。
但是内存映射不能用在某些设备(如网络和终端设备)之间进行复制,并且操作时必须注意文件的长度是否改变。