背景
我们都知道,Nginx是多进程程序,80端口是各进程所共享的,多进程同时listen 80端口,势必会产生竞争,也产生了所谓的“惊群”效应。当内核accept一个连接时,会唤醒所有等待中的进程,但实际上只有一个进程能获取连接,其他的进程都是被无效唤醒的。所以Nginx采用了自有的一套accept加锁机制,避免多个进程同时调用accept。Nginx多进程的锁在底层默认是通过CPU自旋锁来实现。如果操作系统不支持自旋锁,就采用文件锁。
- Tips:查看占用某端口的进程命令lsof -i:80(更准确,master进程和woreker进程都显示了出来) 或者netstat -tunlp | grep 80(只显示出了worker进程)
这里提到了文件锁。那么什么是文件锁呢?
文件锁
在多任务操作系统环境中,如果一个进程尝试对正在被其他进程读取的文件进行写操作,可能会导致正在进行读操作的进程读取到一些被破坏或者不完整的数据;如果两个进程并发对同一个文件进行写操作,可能会导致该文件遭到破坏。因此,为了避免发生这种问题,必须要采用某种机制来解决多个进程并发访问同一个文件时所面临的同步问题,由此而产生了文件加锁方面的技术。
文件锁是一种文件读写机制。在不论什么特定的时间仅仅同意一个进程访问一个文件。利用这样的机制可以使读写单个文件的过程变得更安全。
我们为什么需要文件锁?
想像以下场景:
进程“A”打开和读取一个文件,此文件包括账户相关的一些信息。
进程“B”也打开了这个文件。并读取了文件里的信息。
如今,进程“A”更改了其副本中的一条剩余金额记录,并将其写入文件。
此时,进程“B”并不知道上次读取的文件已经被更改。它还保存着原始的文件副本。然后。进程“B”更改了“A”操作的那条同样的记录,并将记录写入文件。
此时。文件里将仅仅保存了进程“B”更改过的记录。
为了避免这样的事情发生,就要使用文件锁来确保操作的“序列化”。
Linux系统中两种经常使用的文件锁
协同锁
协同锁要求參与操作的进程之间协同合作。
如果进程“A”获得一个WRITE锁,并開始向文件里写入内容;此时,进程“B”并没有试图获取一个锁,它仍然能够打开文件并向文件里写入内容。
在此过程中,进程“B”就是一个非合作进程。如果进程“B”试图获取一个锁,那么整个过程就是一个合作的过程,从而能够保证操作的“序列化”。
仅仅有当參与操作的进程是协同合作的时候,协同锁才干发挥作用。协同锁有时也被称为“非强制”锁
强制锁
强制锁不须要參与操作的进程之间保持协同合作。它利用内核来查检每一个打开、读取、写入操作,从而保证在调用这些操作时不违反文件上的锁规则。关于强制锁的很多其它信息,能够在kernal.org上找到。
为了能使用Linux中的强制锁功能。你须要在文件系统级别上打开它。同时在单个文件上打开它。其步骤是:
挂载文件系统时使用“-o mand”參数。
对于要打开强制锁功能的文件lock_file。必须打开set-group-ID位。关闭group-execute位。
(选择此方法的原因是,当你关闭group-execute时,设置set-group-ID就没有实际的意义了)
###代码1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
int main(int argc, char **argv) {
if(argc > 1) {
int fd = open(argv[1], O_WRONLY);
if(fd == -1){
printf("Unable to open the file\n");
exit(1);
}
static struct flock lock;
lock.l_type = F_WRLCK;
lock.l_start = 0;
lock.l_whence = SEEK_SET;
lock.l_len = 0;
lock.l_pid = getpid();
int ret = fcntl(fd, F_SETLKW, &lock);
printf("Return value of fcntl:%d\n", ret);
if(ret == 0) {
while(1){ scanf("%c", NULL);}
}
}
}
1 |
|
flock的使用场景:检测进程是否已经存在
1 | int checkexit(char* pfile) |
flock()的限制
flock()放置的锁有如下限制
只能对整个文件进行加锁。这种粗粒度的加锁会限制协作进程间的并发。假如存在多个进程,其中各个进程都想同时访问同一个文件的不同部分。
通过flock()只能放置劝告式锁。
很多NFS实现不识别flock()放置的锁。
注释:在默认情况下,文件锁是劝告式的,这表示一个进程可以简单地忽略另一个进程在文件上放置的锁。要使得劝告式加锁模型能够正常工作,所有访问文件的进程都必须要配合,即在执行文件IO之前先放置一把锁。
加锁的原理
从内核实现的角度来看,每当创建一把文件锁的时候,系统就会实例化一个struct file_lock对象,这个file_lock对象会记录锁的相关信息:如锁的类型(共享锁,独占锁)、拥有这把锁的进程号、锁的标识(租赁锁,阻塞锁,POSIX锁,FLOCK锁),等等。最后把这个file_lock对象插入到被锁文件的inode.i_flock链表中,就完成了对该文件的加锁功能。要是其它进程想要对同一个文件加锁,那么它在将file_lock对象插入到inode.i_flock之前,会遍历该链表,如果没有发现冲突的锁,就将其插入到链表尾,表示加锁成功,否则失败。
至于为什么要将inode与file_lock以链表的形式关联起来,主要是考虑到用户有时可以对同一个文件加多个文件锁。例如:我们可以对同一个文件加多个共享锁;或者我们可以同时对文件加POSIX锁和FLOCK锁,这两种锁分别对应flock()和fcntl()两种系统调用函数;再或者可以通过多次调用fcntl()对同一个文件中的多个内容块加上POSIX记录锁。
POSIX锁和FLOCK锁的区别:
POSIX锁和FLOCK锁分别是通过fcntl()和flock()系统调用完成的。虽然实现的原理上都差不多,都是生成一个file_lock对象并插入inode文件锁链表,但是POSIX锁是支持针对某一段文件内容进行加锁的,而FLOCK锁不支持。
POSIX锁可以重复加锁,即同一个进程,可以对同一个文件多次加同样一把锁。例如:第一次我对A文件的一个0~10的内容块加了一把独占锁,那么第二次同一个进程中我一样可以对这个A文件的0~10的内容块再加一把独占锁,这个有点像是递归加锁,但是我解锁时只需要解一次。FLOCK锁则不同,如果你第一次对A文件加了一把独占锁,那么在同一个进程中你就不能对A文件再加一把锁了。这个区别其实只不过是在加锁的时候,遍历inode.i_flock链表时,发现存在PID相同的锁时,系统对于POSIX锁和FLOCK锁的具体处理手段不一样罢了。
通过第2点,我们可以想象一下,POSIX锁和FLOCK锁在多线程环境下的不同。我们知道从Linux内核的视角来看,它是不区分所谓的进程和线程的,都不过是CPU调度队列中的一个个task_struct实例而已,所以不会对线程的场景进行专门的处理,也正以为如此,平时我们用的NPTL线程库也都是在用户态环境中模拟出来的,Linux内核并不直接支持。回到刚刚的话题,因为内核它在加锁的时候是看PID的,所以在内核看来多线程的加锁只不过是同一个进程(因为每个线程的PID都是一样的)在对同一个文件加多把锁。这样,多线程环境下的加锁行为就表现为:同一个进程中的多个线程可以对同一个文件加多次POSIX独占或共享锁,但是不可以对同一个文件加多次FLOCK独占锁(不过共享锁是可以加多次的)。
在一个项目中使用了GPFS共享文件系统,我们在开发过程中发现,对于FLOCK锁只支持本地,而POSIX锁则可以支持跨主机加锁。例如:我们有两台独立的机器A和B,在A机器上有某个进程对文件f加POSIX独占锁,然后在B机器上当有某个进程想对f加POSIX独占锁时,就会失败。可是当我们使用FLOCK锁时,就发现两台机器对同一个文件加FLOCK锁是互不影响的,即A和B机器都可以对f加独占锁。