分类: LINUX
2011-03-16 20:52:35
fcntl有强大的功能,它能够复制一个现有的描述符,获得/设置文件描述符标记,获得/设置文件状态标记,获得/设置异步I/O所有权,获得/设置纪录锁。
当多个用户共同使用,操作一个文件的情况,Linux通常采用的方法就是给文件上锁,来避免共享资源产生竞争的状态。
fcntl文件锁有两种类型:建议性锁和强制性锁
建议性锁是这样规定的:每个使用上锁文件的进程都要检查是否有锁存在,当然还得尊重已有的锁。内核和系统总体上都坚持不使用建议性锁,它们依靠程序员遵守这个规定。(Linux默认是采用建议性锁)
强制性锁是由内核执行的。当文件被上锁来进行写入操作时,在锁定该文件的进程释放该锁之前,内核会阻止任何对该文件的读或写访问,每次读或写访问都得检查锁是否存在。
使用fcntl文件锁进行I/O操作必须小心:进程在开始任何I/O操作前如何去处理锁,在对文件解锁前如何完成所有的操作,是必须考虑的。如果在设置锁之前打开文件,或者读取该锁之后关闭文件,另一个进程就可能在上锁/解锁操作和打开/关闭操作之间的几分之一秒内访问该文件。当一个进程对文件加锁后,无论它是否释放所加的锁,只要文件关闭,内核都会自动释放加在文件上的建议性锁(这也是建议性锁和强制性锁的最大区别), 所以不要想设置建议性锁来达到永久不让别的进程访问文件的目的(强制性锁才可以)^_^;强制性锁则对所有进程起作用。
2.互斥锁和条件变量互斥锁的主要用途:保护临界区(critical region)在任何时候只有一个线程在执行其中的代码。任何时刻只有一个线程可以锁住一个给定的互斥锁。如果这个互斥锁在共享内存区,就可以用来让进程向线程一样的同步(如果这样的话,线程的所有情况都适用进程,下文不再显示指明这点)。实质上,互斥锁的用途就是保护临界区内共享的数据。
互斥锁的初始化:必须在定义互斥锁时初始化。
1.静态分配可以初始化为常值PTHREAD_MUTEX_INITIALIZER(timed)。其值为: { { 0, 0, 0, 0, 0, { 0 } } }。
其他的静态初始化方法:
PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP(recursive,锁多少次就得打开多少次)
PTHREAD_ADAPTIVE_MUTEX_INITIALIZER_NP(fast,用户自己保证正确性)
PTHREAD_ERRORCHECK_MUTEX_INITIALIZER_NP(error check,纠错锁,线程不能去解另一个线程加上的锁)
NP代表 non-portable 不可移植!!
2.动态分配和分配在共享内存区中,运行时调用pthread_mutex_init函数初始化他。别忘记用pthread_mutex_destroy函数释放他。
互斥锁是协作性(cooperative)锁:通俗点说就是,每个线程都得遵守对于临界区的代码必须在获得互斥锁的前提下才可以执行的神圣约定。否则互斥锁,就只是浪费了资源的无用代码。
多个线程等在同一个互斥锁的状况:UNPv2上有这样的描述:不同线程可以有不同的优先级,同步函数(互斥锁、读写锁、信号量)将唤醒优先级最高的被阻塞的线程。
编程技巧:可以把共享数据和互斥锁放到一个结构中。
互斥量的类型:Posix .1定义四种类型:
PTHREAD_MUTEX_NORMAL:不做任何错误检测和死锁检测。
1.同一个线程,在前一次lock操作未解锁,再次加锁,第二次加锁的进程将挂起。
2.同一个线程,解锁未上锁的互斥锁,不会报错。
3.一个线程解锁另外一个线程加锁的互斥锁成功!!!!!!!!(多次测试!!!)
//2.6.35内核、nptl线程库、glibc-2.12.2这个结果和这个帖子一致http://blog.csdn.net/guosha/archive/2008/10/24/3136721.aspx
4.一个线程对互斥锁加锁,另一个线程在对这个互斥锁加锁,另一个线程将会挂起。
5.调度特征是:先等待锁的进程先获得锁。
PTHREAD_MUTEX_RECURSIVE:允许同一线程在互斥量之前对该互斥量进行多次加锁,而且返回错误在出错时,用一个递归互斥量维护锁的计数,要想释放锁就必须解锁和加锁同样多的次数才行^_^。//这个类型不可移植!!!
1.同一个线程,在前一次lock操作未解锁,再次加锁,第二次加锁的进程成功,这是理所当然的。
2.同一个线程,解锁未上锁的互斥锁,返回EPERM 错误。
3.一个线程解锁另外一个线程加锁的互斥锁,会返回EPERM 错误。
4.一个线程对互斥锁加锁,另一个线程在对这个互斥锁加锁,另一个线程将会挂起。
5.调度特征是:先等待锁的进程先获得锁。
PTHREAD_MUTEX_ERRORCHECK:提供错误检查。//不可移植!!!
1.同一个线程,在前一次lock操作未解锁,再次加锁,第二次加锁pthread_mutex_lock将返回非0,即出错!但是用perror输出Success,即错误编号errno被置位。直接输出第二次 pthread_mutex_lock的返回值:Resource deadlock avoided。果然perror有问题,应尽量不用,直接使用函数返回值而不是全局的errno,会避免不少麻烦!!!
2.同一个线程,对为上锁的线程解锁,会返回EPERM 错误。
3.一个线程解锁另外一个线程加锁的互斥锁,会返回EPERM 错误。
4.一个线程对互斥锁加锁,另一个线程在对这个互斥锁加锁,另一个线程将会挂起。不返回错误!!
5.调度特征是:先等待锁的进程先获得锁。
PTHREAD_MUTEX_DEFAULT :linux上这种类型被映射为PTHREAD_MUTEX_NORMAL。
另外:关于初始化方式 是PTHREAD_ADAPTIVE_MUTEX_INITIALIZER_NP的自适应锁
1.同一个线程,在前一次lock操作未解锁,再次加锁,第二次加锁的进程将挂起。
2.同一个线程,解锁未上锁的互斥锁,不会报错,成功。
3.一个线程解锁另外一个线程加锁的互斥锁成功!
4.一个线程对互斥锁加锁,另一个线程在对这个互斥锁加锁,另一个线程将会挂起。
5.调度特征是:所有等待锁的线程自由竞争。
#include
int pthread_mutex_init (pthread_mutex_t *__mutex, __const pthread_mutexattr_t *__mutexattr);
int pthread_mutex_destroy (pthread_mutex_t *__mutex);
int pthread_mutex_trylock (pthread_mutex_t *__mutex);
int pthread_mutex_lock (pthread_mutex_t *__mutex);
int pthread_mutex_timedlock (pthread_mutex_t *__restrict __mutex, __const struct timespec *__restrict__abstime);
int pthread_mutex_unlock (pthread_mutex_t *__mutex);
int pthread_mutexattr_init (pthread_mutexattr_t *__attr);
int pthread_mutexattr_destroy (pthread_mutexattr_t *__attr);
int pthread_mutexattr_getpshared (__const pthread_mutexattr_t *__restrict __attr,int *__restrict __pshared);
int pthread_mutexattr_setpshared (pthread_mutexattr_t *__attr,int __pshared);//设置是进程间还是线程间共享互斥量
int pthread_mutexattr_gettype (__const pthread_mutexattr_t *__restrict __attr, int *__restrict __kind);
int pthread_mutexattr_settype (pthread_mutexattr_t *__attr, int __kind);//设置互斥量类型
条件变量的作用: 互斥锁用于上锁,条件变量用来等待。
int pthread_cond_wait (pthread_cond_t *__restrict __cond, pthread_mutex_t *__restrict __mutex);
int pthread_cond_signal (pthread_cond_t *__cond);//只唤醒等待在相应条件变量上的一个线程。
int pthread_cond_broadcast (pthread_cond_t *__cond);//广播:唤醒等待在相应条件变量上的所有线程。
int pthread_cond_timedwait (pthread_cond_t *__restrict __cond,pthread_mutex_t *__restrict __mutex,__const struct timespec*__restrict __abstime);//定时等待
这里简要说下pthread_cond_signal和pthread_cond_wait:可以这样理解原理上pthread_cond_wait相对于pause函数,pthread_cond_signal函数则相对于产生了一个信号。这样就很好理解了 ^_^。
pthread_cond_wait的原理:调用线程把一个条件变量和锁住的互斥量传给pthread_cond_wait。内核为每个条件变量维护一个等待线程列表,之后pthread_cond_wait函数把调用线程自身放到这个表上。之后,解锁传进来的互斥锁,这两个操作是原子的。 线程进入休眠状态。直到pthread_cond_signal通过参数条件变量,唤醒这个条件变量等待线程列表上的一个线程(优先级最高的,未测试)。之后pthread_cond_wait的调用线程从睡眠中醒过来,把互斥锁锁上,函数返回。
3.读写锁简而言值,读写锁就是把互斥锁的颗粒读再细分,把访问共享区的数据的操作区别出读与写了。其他的没什么好说的!看man手册和UNPv2就够了!!
4.Posix记录上锁概述: 记录上锁是读写的一种扩展类型,他可以用于有或无亲缘的进程间共享的文件的读写操作,同时提供锁住文件不同部分的不同大小的功能.上锁这段文件有个属主的属性他就是上锁的进程的PID。Posix记录上锁的最小范围是一个字节。特别的当指定锁住区域大小为0时:将锁住从从文件的偏移开始到文件的结尾(适用于锁住不断在文件尾部添加数据的情况。)倘若,指定的文件偏移量为0,那么此时将锁住整个文件——文件上锁——记录上锁的特例。锁的长度和文件的偏移量指定由struct flock的l_start 和l_len两个成员承担!!
#include
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) */
};
int fcntl (int __fd, int __cmd, ...);//cmd 对应的参数F_SETLK,F_SETLKW,F_GETLK.
1.对于同一个进程,后执行的F_SETLK,F_SETLKW的命令将覆盖前一次针对有重叠的区域的命令。
2.对于文件中的任意字节只能有一种类型的锁。
3.对于文件中的任意字节可有多个读锁(共享锁),但只能有一个写锁(独占锁)。
4.对于为读而打开的描述符,如果我们设置写锁时,就会有错误。同理,对于为写而打开的描述符,如果设置读锁就会出错!EBADF!!当指定O_WRONLY打开文件而得的描述符,不能设置读锁。
5.锁和进程的PID关联非常紧密:通过l_pid成员标注。这一点非常重要!!
6.饥饿状态:当不断有读锁被加上,可能造成写锁永远不能被设置的竞争状态!!
关于cmd参数:
F_GETLK:测试一个struct flcok结构描述的锁是否会被阻止。对于同一个进程,这个命令永远都只会返回l_type==F_UNLCK,因为同一个进程永远都会覆盖上一次的命令,也就是说不会阻止F_GETLK命令中描述的锁。当pthread_creat创建的新线程也是属于这个进程,所以F_GETLK命令无效!!fork则不同,父子进程的PID不同呀^_^。锁和进程的PID关联非常紧密哟:-)。
F_SETLK:设置一把锁。
F_GETLKW:W是wait。F_GETLK的阻塞版。
5.锁的隐含继承和释放1.锁与进程:当进程终止时(隐含关闭了所有文件描述符),他所建立的锁全部释放!这是理所当然的。
2.锁与文件描述符:如果一个进程多次打开同一个文件获得了多个文件描述符,当关闭其中的任意一个文件描述符A,则这个进程设置的所有锁全部释放(不只是通过文件描述符A设置的锁)。
3.fork之后:子进程和父进程的PID不同,所以父进程的锁对于子进程来说,只是阻碍而已,不会持有父进程的锁。
4.exec之后:进程的PID未变所以仍然持有父进程的锁。这里有个例外,当调用exec函数时如果对个文件描述符指定了close-on-exec标志,相应的文件的和这个进程PID关联的锁就都被释放了,其他进程的锁不变。
6.关于Linux内核中锁的实现:参看这两个链接:
http://www.ibm.com/developerworks/cn/linux/l-cn-filelock/index.html
http://blog.chinaunix.net/u1/57145/showart_454511.html
大致原理是:当关闭一个文件描述符时,内核会从这个描述符关联的i节点开始,检查一个存在的锁的链表,释放掉和调用进程相关的锁通过l_pid成员对应的内核中的是fl_pid(不确定!!)。所以在一个进程中内核就会关闭和这个进程所关联的所有锁,而不会检测这个描述符是否是该进程打开该文件而得到的最后一个描述符。这就是上面2.锁与文件描述符的原因^_^!!
7.建议性锁
建议性锁,就是一种软弱的锁——必须要在参与所有共享数据操作的所有进程之间都遵守这样神圣的约定的前提,建议性的锁才发挥作用,而劝告性的建议锁对于协作进程来说已经足够了!!但是,有读写权限的进程,不遵守这个约定,就会把这一切都搞杂,约定对于他们来说,不好用。但建议性锁也有他的优势:相对与强制性锁,性能好一些。个人理解。
协作进程(cooperating processes):如果每个进程所使用的函数都以一致的方法处理记录锁,互斥锁等,则这些进程被称为协作进程。
8.强制性锁前提:
先执行这个命令
mount -o mand /dev/sda7 /mnt //sda7对应的文件系统格式一定是ext系列才行
注:我的Ubuntu10.10 在挂载的文件系统里创建的文件(即下文的 touch /mnt/firo )权限是777并且无法更改 囧!!!
这里的原因是:因为Ubuntu桌面版用户一般是安装在windows7 下的,那么一般所有硬盘的分区就都是NTFS FAT32而不是EXT系列系统,
所以我mount了一个NTFS文件系统的盘符后,因为NTFS和EXT很不同 ,他没有相关权限位的数据结构 所以并不支持chmod chown等操作。
这里给出一个解决办法:利用/dev/loop1这个设备,创建一个ext2的文件系统之后在挂载,^_^具体命令如下。
dd if=/dev/zero of=/file bs=1k count=10 //其中“/file”是在/目录下的任意文件,可以不存在 。bs是块大小1024字节。count是块数:10个。
losetup /dev/loop1 /file
mkfs -t ext2 /dev/loop1 100 //这里最后一个参数:57是最小的可以指定的值,如果小于57将不能创建文件系统。他指定文件系统使用块数。
mount -t ext2 /dev/loop1 /mnt //但是当只有上面的最后一个参数是100的整数倍的数(比如200)才行。关于这几个命令:大家参看man吧!!!如果mount | grep mnt输出的是如下就对了!!:-)。/dev/loop1 on /mnt type ext2 (rw,mand)之后,修改要加强制锁的文件的权限:设置 SGID 位,并清除组可执行位。这种组合通常来说是毫无意义的,系统用来表示该文件被加了强制锁。例如:touch /mnt/firo
ls -l /mnt/firo
chmod g+s /mnt/firo
chmod g-x /mnt/firo
ls -l /mnt/firo
之后在程序中直接操作这个文件就行了^_^。1.如果想要打开一个有强制性记录锁的文件,而且open函数中指定了O_TRUNC时,即便没有指定O_NONBLOCK,open调用也会立即出错返回,errno置为EAGAIN。
2.强制性锁可以避开。但意义不大,原理是创建个新文件,并删除(unlink不受强制性影响)原有的文件。
3.强制性锁虽然解决非协作进程来捣乱的问题,但是,对于多个进程更新共享文件时,对共享数据仍需要某种锁。具体见UNPv2的9.5。
最后关于lockf和flock
参见这里和man
http://shihaiyang.javaeye.com/blog/482656
http://www.ibm.com/developerworks/cn/linux/l-cn-filelock/index.html
9.lockf()System V的锁函数 lockf()具有如下的形式:
lock()用于对文件施加建议性锁
#include
int lockf(int fd, int function, long size);
参数 fd 是在文件打开操作中获得的文件描述符;
参数 function 可以取如下的参数值:
F_ULOCK 为一个先前锁定的区域解锁
F_LOCK 锁定一个区域
F_TLOCK 测试并锁定一个区域
F_TEST 测试一个区域是否已经上锁。
参数 size 指明了从文件当前位置开始的一段连续锁定区域的长度,当 size 为 0 时,锁定记录将由当前位置一直扩展到文件尾。
函数 lockf()既可以用来上锁有可以用来测试是否已经赏了锁。 如果 lockf 的参数function为 F_LOCK 指定文件的对应区域已被其它进程锁定,那么 lockf 的调用进程将被阻塞直到该区域解锁。上述情况我们称为阻塞。如果在调用 lockf()时把参数设为 F_TLOVK,那么当被测试的区域上了锁时,lockf 便会立即返回-1,出错返回码 errno 将为 EAGAIN,它是一个非阻塞调用。
C代码
#include
my_lock(int fd)
{
/* 将文件指针移回文件头 */
lseek(fd,0L,0);
/* 锁定整个文件 */
if (lockf(fd,F_LOCK,0L)==-1)
{
perror("can't F_LOCK");
exit(1);
}
}
my_unlock(int fd)
{
lseek(fd,0L,0);
if(lockf(fd,F_ULOCK,0L)==-1)
{
perror("can't F_UNLOCK");
exit(1);
}
}
10.flock()BSD UNIX 操作系统提供了如下形式的调用来锁定和解锁一个文件:
#include
int flock(int fd, int operation);
调用 flock有两个参数:
参数 fd 是一个已打开文件的文件描述符;
参数 operation 可设定为下述各值:
LOCK_SH 共享锁
LOCK_EX 互斥锁
LOCK_UN 解锁
LOCK_NB 当文件已被锁定时不阻塞
BSD UNIX使用flock()来请求对指定文件的咨询式锁定和解锁。BSD的咨询锁有共享锁和互斥锁两种。在任一给定时刻,多个进程可以用于属于同一文件的共享锁,但是某共享文件不能同时具有多个互斥锁或存在共享锁和互斥锁共存的情况。如果锁定成功,flock将返回零,否则返回-1。
#include
my_flock(int fd)
{
if (flock(fd,LOCK_EX))==-1)
{
perror(“ can LOCK_EX” );
exit(1);
}
}
my_unload(fd)
{
if (flock(fd,LOCK_UN)==-1)
{
perror(“ can’ t LOCK_UN” );
exit(1);
}
}
11.前面两种锁定方式的比较由于 Linux 支持上面的两种锁定方式,所以可以根据不同的实际情况选用不同的锁定方式。以上的两种锁定方式有以下的不同:
1.System V的锁定方式是记录锁定,可以指定锁定的范围。而 BSD 的锁定方式是文件锁定,只能指定锁定文件。
2.System V的锁定是每个进程所独有的,可以用于父子进程间的共享锁定。而 BSD的锁定方式是可以继承的,父子进程间使用的是同一锁定的,所以不能用于父子进程间的文件共享锁定
可以用fcntl 函数改变一个已打开的文件的属性,可以重新设置读、写、追加、非阻塞等标志(这些标志称为File StatusFlag),而不必重新open 文件。
#include
#include
int fcntl(int fd, int cmd);
int fcntl(int fd, int cmd, long arg);
int fcntl(int fd, int cmd, struct flock *lock);
这个函数和open 一样,也是用可变参数实现的,可变参数的类型和个数取决于前面的cmd 参数。
fcntl函数原型
#include
#include
#include
int fcntl(int fd, //文件描述符
int cmd , //不同的命令
struct flock *lock) //设置记录锁的具体状态
cmd取值:
F_DUPFD 复制文件描述符
F_GETFD 获得fd的close-on-exec标志
F_SETFD 设置close-on-exec标志
F_GETFL 获得open设置标志
F_SETFL 设置lock描述的标志
F_GETLK 测试该锁是否被另外一把锁排斥
F_SETLKW 如果存在其他锁,则调用进程睡眠,如果捕捉到信号则睡眠中断
F_GETOWN 检索收到的SIGIO和SIGURG信号的进程号或者进程组号
F_SETOWN 设置进程号或进程组号
这里的lock结构体如下:
struct flock
{
short l_type; /*F_RDLCK(读取锁),F_WRLCK(写入锁),F_UNLCK(解锁)*/
off_t l_start; /*相对偏移量(字节)*/
short l_whence; /*SEEK_SET ,SEEK_CUR ,SEEK_END */
off_t l_len; /*加锁区域长度*/
pid_t l_pid;
}
成功:0
出错:-1
提示:如果加锁整个文件通常的方法是将l_start设置为0,l_whence设置为SEEK_SET, l_len设置为0。
下面的例子使用F_GETFL和F_SETFL这两种fcntl 命令改变STDIN_FILENO的属性上O_NONBLOCK 选项,实现非阻塞读终端的功能。
用fcntl改变File Status Flag
#include
#include
#include
#include
#include
#define MSG_TRY "try again\n"
int main(void)
{
char buf[10];
int n;
int flags;
flags = fcntl(STDIN_FILENO, F_GETFL);
flags |= O_NONBLOCK;
if (fcntl(STDIN_FILENO, F_SETFL, flags) == -1)
{
perror("fcntl");
exit(1);
}
tryagain:
n = read(STDIN_FILENO, buf, 10);
if (n < 0)
{
if (errno == EAGAIN)
{
sleep(1);
write(STDOUT_FILENO, MSG_TRY,strlen(MSG_TRY));
goto tryagain;
}
perror("read stdin");
exit(1);
}
write(STDOUT_FILENO, buf, n);
return 0;
}