Linux ——IPC 锁

最好的参考资料:

1.师从互联网。

2.UNP v2 Posix IPC的相关章节7、8、9。

3.Linux man 命令。

4.APUE 相关章节11、14。

第一条:概述

                   这里介绍的是Posix.1线程标准的:互斥锁、条件变量、读写锁。他们主要用来同步一个进程内各个线程的,如果把它们放在一个共享内存空间中,Posix允许他们用于进程间同步。如果想了解他们的实现,在libc源码的nptl文件下,你可以找到他们。

                    我同样也会介绍Posix.1标准的基础:fcntl的记录锁。Linux支持记录锁的所有形式:建议性锁、强制性锁、fcntl、lockf、flock。

                    顺便说一下,linux内核提供了类型为struct mutex的经典互斥量,以及相关API在linux/mutex.h文件里可以看到。libc未包含他们,有可移植性更好的Posix的原因把。

第二条:互斥锁和条件变量

互斥锁的主要用途:保护临界区(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<pthread.h>

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的调用线程从睡眠中醒过来,把互斥锁锁上,函数返回。

第三条:读写锁

简而言值,读写锁就是把互斥锁的颗粒读再细分,把访问共享区的数据的操作区别出读与写了。其他的没什么好说的!看man手册和UNPv2

就够了!!

第四条:Posix记录上锁

概述: 记录上锁是读写的一种扩展类型,他可以用于有或无亲缘的进程间共享的文件的读写操作,同时提供锁住文件不同部分的不同大小的功能.上锁这段文件有个属主的属性他就是上锁的进程的PID。Posix记录上锁的最小范围是一个字节。特别的当指定锁住区域大小为0时:将锁住从从文件的偏移开始到文件的结尾(适用于锁住不断在文件尾部添加数据的情况。)倘若,指定的文件偏移量为0,那么此时将锁住整个文件——文件上锁——记录上锁的特例。锁的长度和文件的偏移量指定由struct flock的l_start 和l_len两个成员承担!!

#include<fcntl.h>

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的阻塞版。

锁的隐含继承和释放:

1.锁与进程:当进程终止时(隐含关闭了所有文件描述符),他所建立的锁全部释放!这是理所当然的。

2.锁与文件描述符:如果一个进程多次打开同一个文件获得了多个文件描述符,当关闭其中的任意一个文件描述符A,则这个进程设置的所有锁全部释放(不只是通过文件描述符A设置的锁)。

3.fork之后:子进程和父进程的PID不同,所以父进程的锁对于子进程来说,只是阻碍而已,不会持有父进程的锁。

4.exec之后:进程的PID未变所以仍然持有父进程的锁。这里有个例外,当调用exec函数时如果对个文件描述符指定了close-on-exec标志,相应的文件的和这个进程PID关联的锁就都被释放了,其他进程的锁不变。

 

关于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.锁与文件描述符的原因^_^!!

第五条:建议性锁

建议性锁,就是一种软弱的锁——必须要在参与所有共享数据操作的所有进程之间都遵守这样神圣的约定的前提,建议性的锁才发挥作用,而劝告性的建议锁对于协作进程来说已经足够了!!但是,有读写权限的进程,不遵守这个约定,就会把这一切都搞杂,约定对于他们来说,不好用。但建议性锁也有他的优势:相对与强制性锁,性能好一些。个人理解。

 

协作进程(cooperating processes):如果每个进程所使用的函数都以一致的方法处理记录锁,互斥锁等,则这些进程被称为协作进程。

第六条:强制性锁

前提: 

 

 

先执行这个命令

 

 

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

 

 

 

你可能感兴趣的:(linux,struct,ext,ubuntu,Signal,linux内核)