记录锁控制函数fcntl,可用于有亲缘关系或无亲缘关系的进程间共享某个文件的读与写,共享文件通过文件描述符来访问,这种类型的锁通常在内核中维护,其唯一标识即fcntl函数调用进程的pid。
先以一个例子说明进程间文件共享的问题。
// nolock.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>
#define handle_error_en(en, msg) \
do { errno = en; perror(msg); exit(EXIT_FAILURE); } while (0)
#define FILE_NAME "testfile"
#define MAX_LEN (10)
#define INCRE_TIME (10)
int main(int argc, char *argv[])
{
pid_t pid;
int fd, i;
char line[MAX_LEN + 1];
ssize_t n;
long seqno;
pid = getpid();
if ((fd = open(FILE_NAME, O_RDWR)) == -1) {
handle_error_en(errno, "open");
}
for (i = 0; i < INCRE_TIME; ++i) {
if (lseek(fd, 0, SEEK_SET) == -1) { // rewind before read
handle_error_en(errno, "lseek");
}
if ((n = read(fd, line, MAX_LEN)) == -1) {
handle_error_en(errno, "read");
}
line[n] = '\0'; // null ternimate for sscanf
sscanf(line, "%ld\n", &seqno);
printf("%s: pid = %ld, seq = %ld\n", argv[0], (long)pid, seqno);
++seqno; // increment sequence number
snprintf(line, sizeof(line), "%ld\n", seqno);
if (lseek(fd, 0, SEEK_SET) == -1) { // rewind before write
handle_error_en(errno, "lseek");
}
if (write(fd, line, strlen(line)) == -1) {
handle_error_en(errno, "write");
}
}
if (close(fd) == -1) {
handle_error_en(errno, "close");
}
exit(EXIT_SUCCESS);
}
例子中,有一个共享文件testfile,文件中只有一个数字,初始值为1,程序访问文件时,首先读取共享文件的当前数字并打印出程序名、进程号和这个数字,然后修改数字,递增1,写回到文件,替换原有内容,如此循环10次。当只有一个进程运行这个程序时,是没有问题的,结果如下。
gcc -o nolock nolock .c ./nolock ./nolock: pid = 622, seq = 1 ./nolock: pid = 622, seq = 2 ./nolock: pid = 622, seq = 3 ./nolock: pid = 622, seq = 4 ./nolock: pid = 622, seq = 5 ./nolock: pid = 622, seq = 6 ./nolock: pid = 622, seq = 7 ./nolock: pid = 622, seq = 8 ./nolock: pid = 622, seq = 9 ./nolock: pid = 622, seq = 10
再运行一次。
./nolock ./nolock: pid = 623, seq = 11 ./nolock: pid = 623, seq = 12 ./nolock: pid = 623, seq = 13 ./nolock: pid = 623, seq = 14 ./nolock: pid = 623, seq = 15 ./nolock: pid = 623, seq = 16 ./nolock: pid = 623, seq = 17 ./nolock: pid = 623, seq = 18 ./nolock: pid = 623, seq = 19 ./nolock: pid = 623, seq = 20
可以看出,两个进程先后运行这个程序时,是没有问题的,前一个进程输出结果从1到10,后一个进程输出结果从11到20,但是,如果两个进程同时运行这个程序时,它们将同时访问、修改共享文件,结果就乱了,如下。
./nolock & ./nolock ./nolock: pid = 608, seq = 1 ./nolock: pid = 609, seq = 1 ./nolock: pid = 608, seq = 2 ./nolock: pid = 608, seq = 3 ./nolock: pid = 608, seq = 4 ./nolock: pid = 608, seq = 5 ./nolock: pid = 608, seq = 6 ./nolock: pid = 608, seq = 7 ./nolock: pid = 609, seq = 6 ./nolock: pid = 608, seq = 8 ./nolock: pid = 608, seq = 9 ./nolock: pid = 609, seq = 9 ./nolock: pid = 608, seq = 10 ./nolock: pid = 609, seq = 10 ./nolock: pid = 609, seq = 11 ./nolock: pid = 609, seq = 12 ./nolock: pid = 609, seq = 13 ./nolock: pid = 609, seq = 14 ./nolock: pid = 609, seq = 15 ./nolock: pid = 609, seq = 16
两个进程乱序输出,后一个进程最后的输出结果为16,而不是期待的20,原因是两个进程同时访问共享文件时,没有使用同步机制,下面介绍fcntl函数加锁的用法,并修改上面的程序。
#include <unistd.h>
include <fcntl.h>
int fcntl(int fd, int cmd, ... /* struct flock *arg */ );
fcntl函数出错时返回-1并设置相应的errno,成功时返回值取决于具体的cmd参数。cmd有三个值,F_SETLK、F_SETLKW和F_GETLK,分别表示非阻塞加锁、阻塞加锁和获取锁状态。第三个参数arg是一个flock结构体,可以对文件的某个字节范围加锁,当设置l_whence为SEEK_SET、l_start和l_len都为0时,影响的是整个文件。
struct flock {
short l_type; /* Type of lock: F_RDLCK, F_WRLCK, F_UNLCK */
short l_whence; /* How to interpret l_start: 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 (F_GETLK only) */
};
修改上面的程序:自定义加锁、解锁函数,每次访问共享文件调用lseek前加锁,修改共享文件调用write结束后解锁,为了保证两个进程能够乱序输出,调用my_unlock后sleep一秒。加锁函数my_lock中,flock锁类型为写锁F_WRLCK,设置l_whence、l_start、l_len为SEEK_SET、0、0,保证加锁范围为整个文件,fcntl的cmd为F_SETLKW,阻塞式加锁,这样当两个进程同时运行这个程序时就没有问题了,下面是运行结果。
// fcntllock.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>
#define handle_error_en(en, msg) \
do { errno = en; perror(msg); exit(EXIT_FAILURE); } while (0)
#define FILE_NAME "testfile"
#define MAX_LEN (10)
#define INCRE_TIME (10)
void my_lock(int fd)
{
struct flock lock;
lock.l_type = F_WRLCK;
lock.l_whence = SEEK_SET;
lock.l_start = 0;
lock.l_len = 0;
if (fcntl(fd, F_SETLKW, &lock) == -1) {
handle_error_en(errno, "fcntl lock");
}
}
void my_unlock(int fd)
{
struct flock lock;
lock.l_type = F_UNLCK;
lock.l_whence = SEEK_SET;
lock.l_start = 0;
lock.l_len = 0;
if (fcntl(fd, F_SETLKW, &lock) == -1) {
handle_error_en(errno, "fcntl unlock");
}
}
int main(int argc, char *argv[])
{
pid_t pid;
int fd, i;
char line[MAX_LEN + 1];
ssize_t n;
long seqno;
pid = getpid();
if ((fd = open(FILE_NAME, O_RDWR)) == -1) {
handle_error_en(errno, "open");
}
for (i = 0; i < INCRE_TIME; ++i) {
my_lock(fd); // lock the file
if (lseek(fd, 0, SEEK_SET) == -1) { // rewind before read
handle_error_en(errno, "lseek");
}
if ((n = read(fd, line, MAX_LEN)) == -1) {
handle_error_en(errno, "read");
}
line[n] = '\0'; // null ternimate for sscanf
sscanf(line, "%ld\n", &seqno);
printf("%s: pid = %ld, seq = %ld\n", argv[0], (long)pid, seqno);
++seqno; // increment sequence number
snprintf(line, sizeof(line), "%ld\n", seqno);
if (lseek(fd, 0, SEEK_SET) == -1) { // rewind before write
handle_error_en(errno, "lseek");
}
if (write(fd, line, strlen(line)) == -1) {
handle_error_en(errno, "write");
}
my_unlock(fd); // unlock the file
sleep(1);
}
if (close(fd) == -1) {
handle_error_en(errno, "close");
}
exit(EXIT_SUCCESS);
}
gcc -o fcntllock fcntllock.c ./fcntllock & ./fcntllock ./fcntllock: pid = 3854, seq = 1 ./fcntllock: pid = 3855, seq = 2 ./fcntllock: pid = 3854, seq = 3 ./fcntllock: pid = 3855, seq = 4 ./fcntllock: pid = 3854, seq = 5 ./fcntllock: pid = 3855, seq = 6 ./fcntllock: pid = 3854, seq = 7 ./fcntllock: pid = 3855, seq = 8 ./fcntllock: pid = 3854, seq = 9 ./fcntllock: pid = 3855, seq = 10 ./fcntllock: pid = 3854, seq = 11 ./fcntllock: pid = 3855, seq = 12 ./fcntllock: pid = 3854, seq = 13 ./fcntllock: pid = 3855, seq = 14 ./fcntllock: pid = 3854, seq = 15 ./fcntllock: pid = 3855, seq = 16 ./fcntllock: pid = 3854, seq = 17 ./fcntllock: pid = 3855, seq = 18 ./fcntllock: pid = 3854, seq = 19 ./fcntllock: pid = 3855, seq = 20
后一个进程最后的输出结果为20,而且也没有重复的输出,这个结果是正确的。
Posix记录锁是一种劝告性锁,其含义是内核维护着已由各个进程加锁的所有文件的正确信息,但不能阻止一个进程去写由另一个进程读锁定的共享文件,也不能阻止一个进程去读由另一个进程写锁定的共享文件,也就是说一个进程能够无视劝告性锁而读已写锁定的文件或者写已读锁定的文件。既然如此,那这种劝告性锁还有什么用呢?其实对于协作进程是没有问题的,可以很好的实现文件共享。
对于劝告性锁,有些系统提供了强制性记录锁,使用强制性锁后,内核检查每个read和write请求,以验证其操作不会干扰由某个进程持有的某个锁。对于通常的阻塞式描述字,与某个强制性锁冲突的read或write将把调用线程投入睡眠,直到该锁释放为止。对于非阻塞式描述字,与某个强制性锁冲突的read或write将导致它们返回一个EAGAIN错误。使用强制性锁式,目标文件的组成员执行位必须关掉,SGID位必须打开。需要注意的是,即使强制性锁提供了一定的保护作用,对于非原子的几个操纵也是可能有问题的。