分类: LINUX
2008-12-09 13:44:09
用于锁定文件中任意区域,防止其它进程访问。它是一种针对I/O的同步机制,与互斥等同步机制不同的是,互斥针对代码区域进行锁定,而记录锁直接针对文件进行锁定。记录锁包括建议性锁和强制性锁。
flock(2)可以用来对文件上锁。它是4.2BSD及其之后的BSD版本实现的,Linux、Solaris、AIX等大部分UNIX-like系统也支持这个函数。但这个函数并不是POSIX标准的一部分,书中也没有提。
#include
int flock(int fd, int operation);
该函数将在文件被其它进程锁住时被阻塞,对operation参数逻辑或上LOCK_NB选项可以就可以设置为非阻塞。operation的选项包括LOCK_SH(共享锁,用于读)、LOCK_EX(独占锁,用于写)、LOCK_UN(解锁)。
fcntl(2)也可以用于对文件加上记录锁。
#include
int fcntl(int filedes, int cmd, struct flock *flockptr);
用于记录锁的cmd包括F_GETLK(获取锁的状态)、F_SETLK(以非阻塞方式获得锁)、F_SETLKW(以阻塞方式获得锁)。
flock结构的内容为:
struct flock {
short l_type; /* 包括F_RDLOCK, F_WRLOCK, F_UNLOCK */
off_t l_start; /* 锁的起点 */
short l_whence; /* 包括SEEK_SET, SEEK_CUR, SEEK_END */
off_t l_len; /* 为0时,表示从起点到EOF */
pid_t l_pid; /* 拥有此锁的进程PID */
}
锁定整个文件的常用方法是令flock结构的l_start为0,l_whence为SEEK_SET,l_len为0。要特别注意的是,SEEK_CUR和SEEK_END是随时可能改变的。
同一进程调用fcntl(2)可以多次对同一区域加锁,最新的一次调用生效后将替换同一区域上以前的锁类型(F_RDLOCK/F_WRLOCK/F_UNLOCK)。这意味着多线程使用fcntl加锁是起不到阻塞同步作用的(本人未验证……)。
cmd为F_GETLK时,如果检测的文件区域已经加锁,flockptr指向的对象将更新为该区域的锁状态;如果没有上锁,flockprt->l_type将被置为F_UNLOCK且不会更改该指针指向对象的其它数据。
打开文件时使用了读方式才能加读锁,打开文件时使用了写方式才能加写锁。否则加锁操作将失败并设置errno。
使用fcntl(2)加锁会自动检测死锁,并返回失败和设置errno。
锁的隐含继承与释放:
fork(2)产生的子进程不会继承父进程的锁。即记录锁任何时候只属于一个进程拥有;
进程在exec后依然会继承原来的锁,除非用fcntl(2)对文件设置了close-on-exec标志;
进程终止时,锁自动被释放;
进程关闭一个描述符,只要在该描述符引用的文件上占有锁,该锁也将被释放。而无论是否还存在其它描述符引用该文件。例如使进程的fd1和fd2都引用同一个文件,并使用fd1对文件加锁,关闭了fd2,这时fd1的锁将会被释放。
关于建议性锁和强制性锁:
广义的建议性锁包括互斥等在内的同步机制,但常用来描述进行I/O同步的记录锁;
强制性锁不在POSIX中定义,具体要看fcntl(2)手册中的说明。Linux的手册上指出:使用强制性锁,首先用mount(1)挂载文件系统时要使用mand参数,其次,要锁文件必须关闭组的执行属性(chmod g-x)并打开SetGID(chmod g+s);
强制性锁起作用时,其它进程write(2)资源将直接失败(而建议性锁需要自己调用加锁函数检测),可以利用此特性测试当前挂载的文件系统是否支持强制性锁。
但对文件执行unlink(2)并不受强制性锁的影响,所以强制性锁并不能保证完全的安全;
除了SVR4及其后代如Solaris以外,其它系统上虽然可能也提供STREAMS机制,但不常用。而是使用其历史上的竞争对手,即BSD发明的socket(7)机制(参考ESR的《UNIX编程艺术7.3.1:》)。故略过不看。
对于低速系统调用,需要进行一些有效的设计而使得程序不会由于I/O暂时不可用而被永久阻塞。
使用非阻塞I/O可以使对低速系统调用的使用永远不会被阻塞。打开文件描述符的O_NONBLOCK标志可以使I/O访问不受阻塞。可以在open(2)时直接设置这个标志,也可以在打开后用fcntl(2)设置这个标志。对于一个非阻塞的文件描述符,如果没有数据可读,则read(2)将返回-1并设置errno为EAGAIN。
使用轮询(即无限循环等待)的方式非阻塞的访问I/O资源是可行的,但浪费CPU时间。而专门用一个线程来阻塞等待资源也是可行的,但会由于由于同步的开销增加程序的复杂性。所以对第一段提出的问题,以上两种都是比较笨拙而可能得不偿失的解决方法。
方式是另一种更常见非阻塞的I/O方式。它在设置I/O请求后不用等待而继续执行以后的程序,在资源准备好时才执行I/O操作。
通过信号、线程、轮询、异步I/O的方式进行并行的多I/O操作。都有使程序复杂化、浪费CPU时间或者受到限制之类的缺点。select(2)函数可以用于进行I/O多路转接:
#include
int select(int nfds, fd_set *readfds, fd_set *write_fds, fd_set *exceptfds, struct timeval *timeout);
参数nfds为3个fd_set集里面最大的fd值 + 1,这3个fd_set集用于指定对应的描述符关心的是什么状态,分别为可读、可写和异常。fd_set是不透明的数据类型,它描述了相关的文件描述符集合。操作和测试fd_set的函数包括:
int FD_ISSET(int fd, fd_set *set);
void FD_SET(int fd, fd_set *set);
void FD_CLR(int fd, fd_set *set);
void FD_ZERO(fd_set *set);
select参数中的三个文件描述符集readfds、writefds、exceptfds分别对应可读状态、可写状态、异常状态的描述符集。该函数将使任务阻塞,直到描述符集中的任意一个文件描述符引用的资源对应的状态已经就绪("Ready",例如在writefds中的某个描述符已经可以写入),此时返回三个集合中各文件描述符已就绪状态的和(注意并非文件的总数,例如一个文件描述符同时在writefds和readfds中,且此时既可读又可写时,将统计为2)。并将对应的fd_set指向的对象将被更新为已经可用(准备好)的文件描述符集,它是原来的文件描述符集的一个子集。
参数timeout用于设置阻塞的超时等待时间。如果timeout是0,则select将立即返回;如果timeout指向NULL,select则变为低速系统调用,即将永远阻塞,直到等待到资源可用或者被信号中断。
超时时,select将返回0,同时3个描述符集都将被清零。表示所关心的所有资源都没有准备好。
任何时候只要递送了信号,select将立即返回-1并设置errno为EINTR,3个描述符集不会被更新。
如果令三个fd_set指针参数都为NULL,此时select将类似sleep(2),在阻塞到指定的时间后返回,但可以实现更高的延时精度。
select在测试集合中的某个描述符时如果遇到EOF,则会认为它是可读的。read(2)对此描述符的访问将返回0。
pselect(2)函数是select(2)的变体,它和后者的主要区别是定时采用了纳秒级的timespec结构,且timespec参数声明为const以禁止修改,并可以使用信号集屏蔽不需要的信号:
#include
int pselect(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, const struct timespec *timeout, const sigset_t *sigmask);
poll(2)也可以用于测试文件描述符集并返回它们的状态:
#include
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
参数fds是指向pollfd数组的指针(注意:而不是指向一个pollfd结构),pollfd结构将指定的文件描述符与对其所关心的状态绑定在一起:
struct pollfd
{
int fd;
short events; /* 关心的事件,由用户程序设置 */
short revents; /* 已经发生的事件,由系统设置 */
}
其中events的取值包括了4种读事件,3种写事件,3种异常事件。revents还包括了3种返回事件。
poll被调用后,将阻塞到pollfd数组某个成员的events指定的事件出现时返回,并由此更新revents,直到指定的timeout延时结束。
传统的异步I/O通过捕捉异步信号的方式来实现。如BSD方式的异步I/O,它通过捕捉SIGIO和SIGURG这两个异步信号来进行I/O操作。使用SIGIO时,用fcntl(2)设置F_SETOWN命令指定接收该信号的进程和进程组,并用F_SETFL命令设置所关心的文件描述符的O_ASYNC标志。SIGURG见第十六章。
POSIX扩展了关于异步I/O的库函数,这部分在APUE2中没有提及,而且在开源软件中我没有见过有使用aio函数库的例子。《UNIX System Programming: Communication, Concurrency and Threads》一书中有应用例子。这些库函数包括了
#include
int aio_read(struct aiocb *aiocbp);
int aio_write(struct aiocb *aiocbp);
int aio_cancel(int fd, struct aiocb *aiocbp);
int aio_fsync(int op, struct aiocb *aiocbp);
int aio_suspend(const struct aiocb *const cblist[], int n, const struct timespec *timeout);
int aio_error(const struct aiocb *aiocbp);
ssize_t aio_return(struct aiocb *aiocbp);
结构aiocb定义了进行异步I/O应该指定的数据,例如读/写缓冲区,文件描述符,操作完成时应该产生的信号等。
aio_read和aio_write用于请求异步I/O操作,失败时返回-1并设置errno。
aio_cancel则用于取消对文件描述符fd的异步I/O操作请求,成功时返回AIO_CANCELED。如果I/O正在进行而无法取消,返回AIO_NOTCANCELLED。如果请求的异步I/O均已完成,返回AIO_ALLDONE。否则返回-1。
aio_fsync用于将写入操作异步地同步到磁盘文件,op包括O_SYNC和O_DSYNC。
aio_suspend用于在设置的超时时间内等待指定的aiocb数组中的其中一个异步I/O操作完成。
aio_error返回异步I/O的进度,包括0(已完成)、EINPROGRESS(进行中)、-1(出错)。
aio_return返回I/O完成时实际读/写的字节数,未完成时,返回值是未定义的。
#include
ssize_t readv(int filedes, const struct iovec *iov, int iovcnt);
ssize_t writev(int filedes, const struct iovec *iov, int iovcnt);
readv将从fd中读入的数据按顺序逐块分散存储在iovcnt个缓冲区中,并返回实际读入的字节数。这些缓冲区以iovec结构来描述,并以数组形式(数组长度即为iovcnt)组织起来,iov为数组的头指针。结构iovec定义为
struct iovec
{
void *iov_base; /* 缓冲区起始地址 */
size_t iov_len; /* 缓冲区长度 */
}
writev则将iovcnt个缓冲区的数据按顺序逐块连续的写入fd,并返回实际写入的字节数。
用于将磁盘文件映射到RAM中的缓冲区,这样可以通过直接操作缓冲区读写文件,而不必使用read、write函数。
include
void *mmap(void *addr, size_t len, int prot, int flag, int filedes, off_t off);
addr为指定的缓冲区首址,如果为NULL,则由系统自行分配。也可以使用通过malloc(3)分配到的地址,但这个地址并不能保证被系统使用。len为指定的缓冲区长度;
prot为指定的保护模式,指定了可以对文件执行哪些操作。包括PROT_NONE, PROT_READ, PROT_WRITE, PROT_EXEC的任意或逻辑的组合。但是如果文件的st_mod没有对应的权限,则prot的对应设置不起作用;
flag报可了MAP_FIXED(要求系统使用指定的addr,不建议使用此标志)、MAP_SHARED(修改缓冲区即为直接修改文件)、MAP_PRIVATE(缓冲区仅仅是文件的副本)。
filedes和off指定了指定的文件及文件中的起始位置;
off和addr应为系统虚存页大小(可以用sysconf(3)得到其值)的整数倍;
mmap调用成功后,将返回缓冲区的首址(未必等于addr,除非使用了MAP_FIXED标志);
应注意的是,缓冲区位于进程堆栈中,故fork(2)的子进程将继承该映射区域,但exec后就不再继承。
函数mprotect(2)用于更改缓冲区映射的保护模式:
#include
int mprotect(void *addr, size_t len, int prot);
函数msync(2)则用于使修改后的数据同步映射到的磁盘文件中:
#include
void msync(void *addr, size_t len, int flags);
addr和len为缓冲区的首址和长度,flags指定使用异步方式(MS_ASYNC,将立即返回)还是同步方式(MS_SYNC,同步完成后返回);
解除映射的函数为munmap(2):
#include
int munmap(caddr_t addr, size_t len);
注意:直接关闭fd的话,映射依然有效,不会被解除;另外用munmap解除映射的话不会冲洗缓冲区到文件;
mmap映射方式用于实现文件复制时,效率比read(2)/write(2)高。