版权声明:
本文章内容在非商业使用前提下可无需授权任意转载、发布。
转载、发布请务必注明作者和其微博、微信公众号地址,以便读者询问问题和甄误反馈,共同进步。
微博ID:orroz
微信公众号:Linux系统技术
前沿
使用文件进行进程间通讯应当是最先学会的一种IPC形式。任何编程语言中,文件IO都是很重要的知识,所以使用文件进行进程间通讯就成了很自然被学会的一种手段。考虑到系统对文件本身存在缓存机制,使用文件进行IPC的效率在个别多读少写的情况下并不低下。并且你们虽然时常忘掉IPC的机制可以包括“文件”这一选项。
我们首先引入文件进行IPC,企图先使用文件进行通讯引入一个竞争条件的概念,之后使用文件锁解决这个问题,因而先从文件的角度来管中窥豹的看一下后续相关IPC机制的总体要解决的问题。阅读本文可以帮你解决以下问题:
哪些是竞争条件(racing)?。flock和lockf有哪些区别?flockfile函数和flock与lockf有哪些区别?
怎样使用命令查看文件锁?
假如认为本文有用,请刷二维码随便捐赠。
竞争条件(racing)
我们的第一个反例是多个进程写文件的事例,尽管还没做到通讯,并且这比较便捷的说明一个通讯时常常出现的情况:竞争条件。假定我们要并发100个进程,这种进程约定好一个文件,这个文件初始值内容写0,每一个进程都要打开这个文件读出当前的数字,加一然后将结果写回来。在理想状态下,这个文件最后写的数字应当是100,由于有100个进程打开、读数、加1、写回,自然是有多少个进程最后文件中的数字结果就应当是多少。并且实际上并非这么,可以看一下这个事例:
[zorro@zorrozou-pc0 process]$ cat racing.c
#include
#include
#include
#include
#include
#include
#include
#include
#define COUNT 100
#define NUM 64
#define FILEPATH "/tmp/count"
int do_child(const char *path)
{
/* 这个函数是每个子进程要做的事情
每个子进程都会按照这个步骤进行操作:
1. 打开FILEPATH路径的文件
2. 读出文件中的当前数字
3. 将字符串转成整数
4. 整数自增加1
5. 将证书转成字符串
6. lseek调整文件当前的偏移量到文件头
7. 将字符串写会文件
当多个进程同时执行这个过程的时候,就会出现racing:竞争条件,
多个进程可能同时从文件独到同一个数字,并且分别对同一个数字加1并写回,
导致多次写回的结果并不是我们最终想要的累积结果。 */
int fd;
int ret, count;
char buf[NUM];
fd = open(path, O_RDWR);
if (fd < 0) {
perror("open()");
exit(1);
}
/* */
ret = read(fd, buf, NUM);
if (ret < 0) {
perror("read()");
exit(1);
}
buf[ret] = '';
count = atoi(buf);
++count;
sprintf(buf, "%d", count);
lseek(fd, 0, SEEK_SET);
ret = write(fd, buf, strlen(buf));
/* */
close(fd);
exit(0);
}
int main()
{
pid_t pid;
int count;
for (count=0;count<COUNT;count++) {
pid = fork();
if (pid < 0) {
perror("fork()");
exit(1);
}
if (pid == 0) {
do_child(FILEPATH);
}
}
for (count=0;count<COUNT;count++) {
wait(NULL);
}
}
这个程序做后执行的疗效如下:
[zorro@zorrozou-pc0 process]$ make racing
cc racing.c -o racing
[zorro@zorrozou-pc0 process]$ echo 0 > /tmp/count
[zorro@zorrozou-pc0 process]$ ./racing
[zorro@zorrozou-pc0 process]$ cat /tmp/count
71[zorro@zorrozou-pc0 process]$
[zorro@zorrozou-pc0 process]$ echo 0 > /tmp/count
[zorro@zorrozou-pc0 process]$ ./racing
[zorro@zorrozou-pc0 process]$ cat /tmp/count
61[zorro@zorrozou-pc0 process]$
[zorro@zorrozou-pc0 process]$ echo 0 > /tmp/count
[zorro@zorrozou-pc0 process]$ ./racing
[zorro@zorrozou-pc0 process]$ cat /tmp/count
64[zorro@zorrozou-pc0 process]$
我们执行了三次这个程序,每次结果都不太一样,第一次是71,第二次是61,第三次是64,全都没有得到预期结果,这就是竞争条件(racing)引入的问题。仔细剖析这个进程我们可以发觉这个竞争条件是怎么发生的:
最开始文件内容是0,假定此时同时打开了3个进程,这么她们分别读文件的时侯,这个过程是可能并发的,于是每位进程读到的字段可能都是0,由于她们都在别的进程没写入1之前就开始读了文件。于是三个进程都是给0加1,之后写了个1回到文件。其他进程以这种推,每次100个进程的执行次序可能不一样,于是结果是每次得到的值都可能不太一样,而且一定都多于形成的实际进程个数。于是我们把这些多个执行过程(如进程或线程)中访问同一个共享资源,而这种共享资源又有难以被多个执行过程存取的的程序片断,称作临界区代码。
这么该怎么解决这个racing的问题呢?对于这个事例来说,可以用文件锁的方法解决这个问题。就是说,对临界区代码进行加锁,来解决竞争条件的问题。哪段是临界区代码?在这个事例中,两端//之间的部份就是临界区代码。一个正确的事例是:
...
ret = flock(fd, LOCK_EX);
if (ret == -1) {
perror("flock()");
exit(1);
}
ret = read(fd, buf, NUM);
if (ret < 0) {
perror("read()");
exit(1);
}
buf[ret] = '';
count = atoi(buf);
++count;
sprintf(buf, "%d", count);
lseek(fd, 0, SEEK_SET);
ret = write(fd, buf, strlen(buf));
ret = flock(fd, LOCK_UN);
if (ret == -1) {
perror("flock()");
exit(1);
}
...
我们将临界区部份代码前后都使用了flock的互斥锁,避免了临界区的racing。这个事例其实并没有真正达到让多个进程通过文件进行通讯,解决某种协同工作问题的目的,而且足以表现出进程间通讯机制的一些问题了。当涉及到数据在多个进程间进行共享的时侯,仅仅只实现数据通讯或共享机制本身是不够的,还须要实现相关的同步或异步机制来控制多个进程,达到保护临界区或其他让进程可以处理同步或异步风波的能力。我们可以觉得文件锁是可以实现这样一种多进程的协调同步能力的机制,而不仅文件锁以外,还有其他机制可以达到相同或则不同的功能,我们会在下文中继续详尽解释。
再度,我们并不对flock这个方式本身进行功能性讲解。这些功能性讲解你们可以很轻易的在网上或则通过别的书籍得到相关内容。本文愈发侧重的是Linux环境提供了多少种文件锁以及她们的区别是哪些?
flock和lockf
从底层的实现来说,Linux的文件锁主要有两种:flock和lockf。须要额外对lockf说明的是linux课程,它只是fcntl系统调用的一个封装。从使用角度讲,lockf或fcntl实现了更细细度文件锁,即:记录锁。我们可以使用lockf或fcntl对文件的部份字节上锁,而flock只能对整个文件加锁。这两种文件锁是从历史上不同的标准中起源的linux 进程文件linux文本编辑器,flock来自BSD而lockf来自POSIX,所以lockf或fcntl实现的锁在类型上又称作POSIX锁。
不仅这个区别外,fcntl系统调用还可以支持强制锁(Mandatorylocking)。强制锁的概念是传统UNIX为了强制应用程序遵循锁规则而引入的一个概念,与之对应的概念就是建议锁(Advisorylocking)。我们日常使用的基本都是建议锁,它并不强制生效。这儿的不强制生效的意思是,假如某一个进程对一个文件持有一把锁以后,其他进程依旧可以直接对文件进行各类操作的,例如open、read、write。只有当多个进程在操作文件前都去检测和对相关锁进行锁操作的时侯,文件锁的规则才能生效。这就是通常建议锁的行为。而强制性锁企图实现一套内核级的锁操作。当有进程对某个文件上锁以后,其他进程虽然不在操作文件之前检测锁,也会在open、read或write等文件操作时发生错误。内核将对有锁的文件在任何情况下的锁规则都生效,这就是强制锁的行为。由此可以理解,假如内核想要支持强制锁,将须要在内核实现open、read、write等系统调用内部进行支持。
从应用的角度来说,Linux内核其实堪称具备了强制锁的能力,但其对强制性锁的实现是不可靠的,建议你们还是不要在Linux下使用强制锁。事实上,在我目前手头正在使用的Linux环境上,一个系统在mount-omand分区的时侯报错(archlinuxkernel4.5),而另一个系统似乎可以以强制锁形式mount上分区,然而功能实现却不完整,主要表现在只有在加锁后形成的子进程中open就会报错,倘若直接write是没问题的,但是其他进程无论open还是read、write都没问题(Centos7kernel3.10)。鉴于此,我们就不在此介绍怎样在Linux环境中打开所谓的强制锁支持了。我们只需晓得,在Linux环境下的应用程序,flock和lockf在是锁类型方面没有本质差异,她们都是建议锁,而非强制锁。
假如认为本文有用,请刷二维码随便捐赠。
flock和lockf另外一个差异是它们实现锁的方法不同。这在应用的时侯表现在flock的语义是针对文件的锁,而lockf是针对文件描述符(fd)的锁。我们用一个反例来观察这个区别:
[zorro@zorrozou-pc0 locktest]$ cat flock.c
#include
#include
#include
#include
#include
#include
#include
#include
#define PATH "/tmp/lock"
int main()
{
int fd;
pid_t pid;
fd = open(PATH, O_RDWR|O_CREAT|O_TRUNC, 0644);
if (fd < 0) {
perror("open()");
exit(1);
}
if (flock(fd, LOCK_EX) < 0) {
perror("flock()");
exit(1);
}
printf("%d: locked!n", getpid());
pid = fork();
if (pid < 0) {
perror("fork()");
exit(1);
}
if (pid == 0) {
/*
fd = open(PATH, O_RDWR|O_CREAT|O_TRUNC, 0644);
if (fd < 0) {
perror("open()");
exit(1);
}
*/
if (flock(fd, LOCK_EX) < 0) {
perror("flock()");
exit(1);
}
printf("%d: locked!n", getpid());
exit(0);
}
wait(NULL);
unlink(PATH);
exit(0);
}
里面代码是一个flock的事例,其作用也很简单:
打开/tmp/lock文件。使用flock对其加互斥锁。复印“PID:locked!”表示加锁成功。打开一个子进程,在子进程中使用flock对同一个文件加互斥锁。子进程复印“PID:locked!”表示加锁成功。若果没加锁成功子进程会推出,不显示相关内容。父进程回收子进程并推出。
这个程序直接编译执行的结果是:
[zorro@zorrozou-pc0 locktest]$ ./flock
23279: locked!
23280: locked!
母子进程都加锁成功了。这个结果显然并不符合我们对文件加锁的初衷。根据我们对互斥锁的理解,子进程对父进程早已加锁过的文件应当加锁失败才对。我们可以稍为更改一下里面程序让它达到预期疗效,将子进程代码段中的注释取消掉重新编译即可:
...
/*
fd = open(PATH, O_RDWR|O_CREAT|O_TRUNC, 0644);
if (fd < 0) {
perror("open()");
exit(1);
}
*/
...
将这段代码上下的//删掉重新编译。然后执行的疗效如下:
[zorro@zorrozou-pc0 locktest]$ make flock
cc flock.c -o flock
[zorro@zorrozou-pc0 locktest]$ ./flock
23437: locked!
此时子进程flock的时侯会阻塞,让进程的执行仍然停在这。这才是我们使用文件锁以后预期该有的疗效。而相同的程序使用lockf却不会这样。这个缘由在于flock和lockf的语义是不同的。使用lockf或fcntl的锁,在实现上关联到文件结构体,这样的实现造成锁不会在fork以后毛毯进程承继。而flock在实现上关联到的是文件描述符,这就意味着假如我们在进程中复制了一个文件描述符,这么使用flock对这个描述符加的锁也会在新复制出的描述符中继续引用。在进程fork的时侯,新形成的子进程的描述符也是从父进程承继(复制)来的。在子进程刚开始执行的时侯,母女进程的描述符关系实际上跟在一个进程中使用dup复制文件描述符的状态一样(参见《UNIX环境中级编程》8.3节的文件共享部份)。这就可能导致上述事例的情况,通过fork形成的多个进程,由于子进程的文件描述符是复制的父进程的文件描述符,所以造成母子进程同时持有对同一个文件的互斥锁,造成第一个事例中的子进程依旧可以加锁成功。这个文件共享的现象在子进程使用open重新打开文件以后就不再存在了,所以重新对同一文件open以后,子进程再使用flock进行加锁的时侯会阻塞。另外要注意:除非文件描述符被标记了close-on-exec标记,flock锁和lockf锁都可以穿越exec,在当前进程弄成另一个执行镜像以后一直保留。
里面的事例中只演示了fork所形成的文件共享对flock互斥锁的影响,同样诱因也会造成dup或dup2所形成的文件描述符对flock在一个进程内形成相同的影响。dup引起的锁问题通常只有在多线程情况下才能形成影响,所以应当避开在多线程场景下使用flock对文件加锁,而lockf/fcntl则没有这个问题。
为了对比flock的行为,我们在此列举使用lockf的相同事例,来演示一下它们的不同:
[zorro@zorrozou-pc0 locktest]$ cat lockf.c
#include
#include
#include
#include
#include
#include
#include
#include
#define PATH "/tmp/lock"
int main()
{
int fd;
pid_t pid;
fd = open(PATH, O_RDWR|O_CREAT|O_TRUNC, 0644);
if (fd < 0) {
perror("open()");
exit(1);
}
if (lockf(fd, F_LOCK, 0) < 0) {
perror("lockf()");
exit(1);
}
printf("%d: locked!n", getpid());
pid = fork();
if (pid < 0) {
perror("fork()");
exit(1);
}
if (pid == 0) {
/*
fd = open(PATH, O_RDWR|O_CREAT|O_TRUNC, 0644);
if (fd < 0) {
perror("open()");
exit(1);
}
*/
if (lockf(fd, F_LOCK, 0) < 0) {
perror("lockf()");
exit(1);
}
printf("%d: locked!n", getpid());
exit(0);
}
wait(NULL);
unlink(PATH);
exit(0);
}
编译执行的结果是:
[zorro@zorrozou-pc0 locktest]$ ./lockf
27262: locked!
在子进程不用open重新打开文件的情况下,进程执行一直被阻塞在子进程lockf加锁的操作上。关于fcntl对文件实现记录锁的详尽内容,你们可以参考《UNIX环境中级编程》中关于记录锁的14.3章节。
标准IO库文件锁
C语言的标准IO库中还提供了一套文件锁,它们的原型如下:
#include
void flockfile(FILE *filehandle);
int ftrylockfile(FILE *filehandle);
void funlockfile(FILE *filehandle);
从实现角度来说,stdio库中实现的文件锁与flock或lockf有本质区别。作为一种标准库,其实现的锁必然要考虑跨平台的特点,所以其结构都是在用户态的FILE结构体中实现的linux 进程文件,而非内核中的数据结构来实现。这直接造成的结果就是,标准IO的锁在多进程环境中使用是有问题的。进程在fork的时侯会复制一整套父进程的地址空间,这将造成子进程中的FILE结构与父进程完全一致。就是说,父进程若果加锁了,子进程也将持有这把锁,父进程没加锁,子进程因为地址空间跟父进程是独立的,所以也未能通过FILE结构体检测别的进程的用户态空间是否家了标准IO库提供的文件锁。这些限制造成这套文件锁只能处理一个进程中的多个线程之间共享的FILE的进行文件操作。就是说,多个线程必须同时操作一个用fopen打开的FILE变量,假如内部自己使用fopen重新打开文件,这么返回的FILE*地址不同,也起不到线程的互斥作用。
我们分别将两种使用线程的状态的事例分别列下来,第一种是线程之间共享同一个FILE*的情况,这些情况互斥是没问题的:
[zorro@zorro-pc locktest]$ cat racing_pthread_sharefp.c
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define COUNT 100
#define NUM 64
#define FILEPATH "/tmp/count"
static FILE *filep;
void *do_child(void *p)
{
int fd;
int ret, count;
char buf[NUM];
flockfile(filep);
if (fseek(filep, 0L, SEEK_SET) == -1) {
perror("fseek()");
}
ret = fread(buf, NUM, 1, filep);
count = atoi(buf);
++count;
sprintf(buf, "%d", count);
if (fseek(filep, 0L, SEEK_SET) == -1) {
perror("fseek()");
}
ret = fwrite(buf, strlen(buf), 1, filep);
funlockfile(filep);
return NULL;
}
int main()
{
pthread_t tid[COUNT];
int count;
filep = fopen(FILEPATH, "r+");
if (filep == NULL) {
perror("fopen()");
exit(1);
}
for (count=0;count<COUNT;count++) {
if (pthread_create(tid+count, NULL, do_child, NULL) != 0) {
perror("pthread_create()");
exit(1);
}
}
for (count=0;count<COUNT;count++) {
if (pthread_join(tid[count], NULL) != 0) {
perror("pthread_join()");
exit(1);
}
}
fclose(filep);
exit(0);
}
另一种情况是每位线程都fopen重新打开一个描述符,此时线程是不能互斥的:
[zorro@zorro-pc locktest]$ cat racing_pthread_threadfp.c
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define COUNT 100
#define NUM 64
#define FILEPATH "/tmp/count"
void *do_child(void *p)
{
int fd;
int ret, count;
char buf[NUM];
FILE *filep;
filep = fopen(FILEPATH, "r+");
if (filep == NULL) {
perror("fopen()");
exit(1);
}
flockfile(filep);
if (fseek(filep, 0L, SEEK_SET) == -1) {
perror("fseek()");
}
ret = fread(buf, NUM, 1, filep);
count = atoi(buf);
++count;
sprintf(buf, "%d", count);
if (fseek(filep, 0L, SEEK_SET) == -1) {
perror("fseek()");
}
ret = fwrite(buf, strlen(buf), 1, filep);
funlockfile(filep);
fclose(filep);
return NULL;
}
int main()
{
pthread_t tid[COUNT];
int count;
for (count=0;count<COUNT;count++) {
if (pthread_create(tid+count, NULL, do_child, NULL) != 0) {
perror("pthread_create()");
exit(1);
}
}
for (count=0;count<COUNT;count++) {
if (pthread_join(tid[count], NULL) != 0) {
perror("pthread_join()");
exit(1);
}
}
exit(0);
}
以上程序你们可以自行编译执行瞧瞧疗效。
文件锁相关命令
系统为我们提供了flock命令,可以便捷我们在命令行和shell脚本中使用文件锁。须要注意的是,flock命令是使用flock系统调用实现的,所以在使用这个命令的时侯请注意进程关系对文件锁的影响。flock命令的使用方式和在脚本编程中的使用可以参见我的另一篇文章《shell编程之常用方法》中的bash并发编程和flock这部份内容,在此不在赘言。
我们还可以使用lslocks命令来查看当前系统中的文件锁使用情况。一个常见的现实如下:
[root@zorrozou-pc0 ~]# lslocks
COMMAND PID TYPE SIZE MODE M START END PATH
firefox 16280 POSIX 0B WRITE 0 0 0 /home/zorro/.mozilla/firefox/bk2bfsto.default/.parentlock
dmeventd 344 POSIX 4B WRITE 0 0 0 /run/dmeventd.pid
gnome-shell 472 FLOCK 0B WRITE 0 0 0 /run/user/120/wayland-0.lock
flock 27452 FLOCK 0B WRITE 0 0 0 /tmp/lock
lvmetad 248 POSIX 4B WRITE 0 0 0 /run/lvmetad.pid
这其中,TYPE主要表示锁类型,就是上文我们描述的flock和lockf。lockf和fcntl实现的锁事POSIX类型。M表示是否事强制锁,0表示不是。若果是记录锁的话,START和END表示锁住文件的记录位置,0表示目前锁住的是整个文件。MODE主要拿来表示锁的权限,实际上这也说明了锁的共享属性。在系统底层,互斥锁表示为WRITE,而共享锁表示为READ,假如这段出现*则表示有其他进程正在等待这个锁。其余参数可以参考manlslocks。
最后
本文通过文件盒文件锁的事例,引出了竞争条件这样在进程间通讯中须要解决的问题。并深入剖析了系统编程中常用的文件锁的实现和应用特征。希望你们对进程间通讯和文件锁的使用有更深入的理解。
假如认为本文有用,请刷二维码随便捐赠。
你们好,我是Zorro!
假如你喜欢本文,欢迎在微博上搜索“orroz”关注我,地址是:
你们也可以在陌陌上搜索:Linux系统技术关注我的公众号。
我的所有文章就会沉淀在我的个人博客上,地址是:。
欢迎使用以上各类形式一起阐述学习,共同进步。
公众号二维码: