对于锁这个字,大家一定不会陌生,因为我们生活中就存在着大量的锁,它们各个方面发挥着它的作用,现在世界中的锁的功能都可归结为一句话,就是阻止某些人做某些事,例如,门锁就是阻止除了屋主之外的人进入这个房子,你进入不到这个房子,也就不能使用房子里面的东西。
而因为程序经常需要共享数据,而这通常又是通过文件来实现的,试想一个情况,A进程正在对一个文件进行写 *** 作,而另一个程序B需要对同一个文件进行读 *** 作,并以读取到的数据作为自己程序运行时所需要的数据,这会发生什么情况呢?进程B可能会读到错乱的数据,因为它并不知道另一个进程A正在改写这个文件中的数据。
为了解决类似的问题,就出现了文件锁定,简单点来说,这是文件的一种安全的更新方式,当一个程序正在对文件进行写 *** 作时,文件就会进入一种暂时状态,在这个状态下,如果另一个程序尝试读这个文件,它就会自动停下来等待这个状态结束。Linux系统提供了很多特性来实现文件锁定,其中最简单的方法就是以原子 *** 作的方式创建锁文件。
用回之前的例子就是,文件锁就是当文件在写的时候,阻止其他的需要写或者要读文件的进程来 *** 作这个文件。
二、创建锁文件
创建一个锁文件是非常简单的,我们可以使用open系统调用来创建一个锁文件,在调用open时oflags参数要增加参数O_CREAT和O_EXCL标志,如file_desc = open("/tmp/LCK.test", O_RDWR|O_CREAT|O_EXCL, 0444)就可以创建一个锁文件/tmp/LCK.test。O_CREAT|O_EXCL,可以确保调用者可以创建出文件,使用这个模式可以防止两个程序同时创建同一个文件,如果文件(/tmp/LCK.test)已经存在,则open调用就会失败,返回-1。
如果一个程序在它执行时,只需要独占某个资源一段很短的时间,这个时间段(或代码区)通常被叫做临界区,我们需要在进入临界区之前使用open系统调用创建锁文件,然后在退出临界区时用unlink系统调用删除这个锁文件。
注意:锁文件只是充当一个指示器的角色,程序间需要通过相互协作来使用它们,也就是说锁文件只是建议锁,而不是强制锁,并不会真正阻止你读写文件中的数据。
可以看看下面的例子:源文件文件名为filelock1.c,代码如下:
#include <unistd.h>#include <stdlib.h>#include <stdio.h>#include <fcntl.h>#include <errno.h>int main() { const char *lock_file = "/tmp/LCK.test1"int n_fd = -1int n_tries = 10while(n_tries--) { //创建锁文件 n_fd = open(lock_file, O_RDWR|O_CREAT|O_EXCL, 0444)if(n_fd == -1) { //创建失败 printf("%d - Lock already present ", getpid())sleep(2)} else { //创建成功 printf("%d - I have exclusive access ", getpid())sleep(1)close(n_fd)//删除锁文件,释放锁 unlink(lock_file)sleep(2)} } return 0}
同时运行同一个程序的两个实例,运行结果为:
从运行的结果可以看出两个程序交叉地对对文件进行锁定,但是真实的 *** 作却是,每次调用open函数去检查/tmp/LCK.test1这个文件是否存在,如果存在open调用就失败,显示有进程已经把这个文件锁定了,如果这个文件不存在,就创建这个文件,并显示许可信息。但是这种做法有一定的缺憾,我们可以看到文件/tmp/LCK.test1被创建了很多次,也被unlink删除了很多次,也就是说我们不能使用已经事先有数据的文件作为这种锁文件,因为如果文件已经存在,则open调用总是失败。
给我的感觉是,这更像是一种对进程工作的协调性安排,更像是二进制信号量的作用,文件存在为0,不存在为1,而不是真正的文件锁定。
三、区域锁定
我们还有一个问题,就是如果同一个文件有多个进程需要对它进行读写,而一个文件同一时间只能被一个进程进行写 *** 作,但是多个进程读写的区域互不相关,如果总是要等一个进程写完其他的进程才能对其进行读写,效率又太低,那么是否可以让多个进程同时对文件进行读写以提高数据读写的效率呢?
为了解决上面提到的问题,和出现在第二点中的问题,即不能把文件锁定到指定的已存在的数据文件上的问题,我们提出了一种新的解决方案,就是区域锁定。
简单点来说,区域锁定就是,文件中的某个部分被锁定了,但其他程序可以访问这个文件中的其他部分。
然而,区域锁定的创建和使用都比上面说的文件锁定复杂很多。
1、创建区域锁定
在Linux上为实现这一功能,我们可以使用fcntl系统调用和lockf调用,但是下面以fcntl系统调用来讲解区域锁定的创建。
fctnl的函数原理为:
int fctnl(int fildes, int command, ...)
它对一个打开的文件描述进行 *** 作,并能根据command参数的设置完成不同的任务,它有三个可选的任务:F_GETLK,F_SETLK,F_SETLKW,至于这三个参数的意义下面再详述。而当使用这些命令时,fcntl的第三个参数必须是一个指向flock结构的指针,所以在实际应用中,fctnl的函数原型一般为:int fctnl(int fildes, int command, struct flock *flock_st)
2、flock结构
准确来说,flock结构依赖具体的实现,但是它至少包括下面的成员:
short l_type文件锁的类型,对应于F_RDLCK(读锁,也叫共享锁),F_UNLCK(解锁,也叫清除锁),F_WRLCK(写锁,也叫独占锁)中的一个。
short l_whence从文件的哪个相对位置开始计算,对应于SEEK_SET(文件头),SEEK_CUR(当前位置),SEEK_END(文件尾)中的一个。
off_t l_start从l_whence开始的第l_start个字节开始计算。
off_t l_len锁定的区域的长度。
pid_t l_pid用来记录参持有锁的进程。
成员l_whence、l_start和l_len定义了一个文件中的一个区域,即一个连续的字节集合,例如:
struct flock region
region.l_whence = SEEK_SET
region.l_start = 10
region.l_len = 20
则表示fcntl函数 *** 作锁定的区域为文件头开始的第10到29个字节之间的这20个字节。
3、文件锁的类型
从上面的flock的成员l_type的取值我们可以知道,文件锁的类型主要有三种,这里对他们进行详细的解说。
F_RDLCK:
从它的名字我们就可以知道,它是一个读锁,也叫共享锁。许多不同的进程可以拥有文件同一(或重叠)区域上的读(共享)锁。而且只要任一进程拥有一把读(共享)锁,那么就没有进程可以再获得该区域上的写(独占)锁。为了获得一把共享锁,文件必须以“读”或“读/写”方式打开。
简单点来说就是,当一个进程在读文件中的数据时,文件中的数据不能被改变或改写,这是为了防止数据被改变而使读数据的程序读取到错乱的数据,而文件中的同一个区域能被多个进程同时读取,这是容易理解的,因为读不会破坏数据,或者说读 *** 作不会改变文件的数据。
F_WRLCK:
从它的名字,我们就可以知道,它是一个写锁,也叫独占锁。只有一个进程可以在文件中的任一特定区域拥有一把写(独占)锁。一旦一个进程拥有了这样一把锁,任何其他进程都无法在该区域上获得任何类型的锁。为了获得一把写(独占)锁,文件也必须以“读”或“读/写”方式打开。
简单点来说,就是一个文件同一区域(或重叠)区域进在同一时间,只能有一个进程能对其进行写 *** 作,并且在写 *** 作进行期间,其他的进程不能对该区域进行读取数据。这个要求是显然易见的,因为如果两个进程同时对一个文件进行写 *** 作,就会使文件的内容错乱起来,而由于写时会改变文件中的数据,所以它也不允许其他进程对文件的数据进行读取和删除文件等 *** 作。
F_UNLCK:
从它的名字就可以知道,它用于把一个锁定的区域解锁。
4、不同的command的意义
在前面说到fcntl函数的command参数时,说了三个命令选项,这里将对它们进行详细的解说。
F_GETLK命令,它用于获取fildes(fcntl的第一个参数)打开的文件的锁信息,它不会尝试去锁定文件,调用进程可以把自己想创建的锁类型信息传递给fcntl,函数调用就会返回将会阻止获取锁的任何信息,即它可以测试你想创建的锁是否能成功被创建。fcntl调用成功时,返回非-1,如果锁请求可以成功执行,flock结构将保持不变,如果锁请求被阻止,fcntl会用相关的信息覆盖flock结构。失败时返回-1。
所以,如果调用成功,调用程序则可以通过检查flock结构的内容来判断其是否被修改过,来检查锁请求能否被成功执行,而又因为l_pid的值会被设置成拥有锁的进程的标识符,所以大多数情况下,可以通过检查这个字段是否发生变化来判断flock结构是否被修改过。
使用F_GETLK的fcntl函数调用后会立即返回。
举个例子来说,例如,有一个flock结构的变量,flock_st,flock_st.l_pid = -1,文件的第10~29个字节已经存在一个读锁,文件的第40~49个字节中已经存在一个写锁,则调用fcntl时,如果用F_GETLK命令,来测试在第10~29个字节中是否可以创建一个读锁,因为这个锁可以被创建,所以,fcntl返回非-1,同时,flock结构的内容也不会改变,flock_st.l_pid = -1。而如果我们测试第40~49个字节中是否可以创建一个写锁时,由于这个区域已经存在一个写锁,测试失败,但是fcntl还是会返回非-1,只是flock结构会被这个区域相关的锁的信息覆盖了,flock_st.l_pid为拥有这个写锁的进程的进程标识符。
F_SETLK命令,这个命令试图对fildes指向的文件的某个区域加锁或解锁,它的功能根据flock结构的l_type的值而定。而对于这个命令来说,flock结构的l_pid字段是没有意义的。如果加锁成功,返回非-1,如果失败,则返回-1。使用F_SETLK的fcntl函数调用后会立即返回。
F_SETLKW命令,这个命令与前面的F_SETLK,命令作用相同,但不同的是,它在无法获取锁时,即测试不能加锁时,会一直等待直到可以被加锁为止。
5、例子
看了这么多的说明,可能你已经很乱了,就用下面的例子来整清你的思想吧。
源文件名为filelock2.c,用于创建数据文件,并将文件区域加锁,代码如下:
#include <unistd.h>#include <stdlib.h>#include <stdio.h>#include <fcntl.h>int main() { const char *test_file = "test_lock.txt"int file_desc = -1int byte_count = 0char *byte_to_write = "A"struct flock region_1struct flock region_2int res = 0//打开一个文件描述符 file_desc = open(test_file, O_RDWR|O_CREAT, 0666)if(!file_desc) { fprintf(stderr, "Unable to open %s for read/write ", test_file)exit(EXIT_FAILURE)} //给文件添加100个‘A’字符的数据 for(byte_count = 0byte_count <100++byte_count) { write(file_desc, byte_to_write, 1)} //在文件的第10~29字节设置读锁(共享锁) region_1.l_type = F_RDLCKregion_1.l_whence = SEEK_SETregion_1.l_start = 10region_1.l_len = 20//在文件的40~49字节设置写锁(独占锁) region_2.l_type = F_WRLCKregion_2.l_whence = SEEK_SETregion_2.l_start = 40region_2.l_len = 10printf("Process %d locking file ", getpid())//锁定文件 res = fcntl(file_desc, F_SETLK, ®ion_1)if(res == -1) { fprintf(stderr, "Failed to lock region 1 ")} res = fcntl(file_desc, F_SETLK, ®ion_2)if(res == -1) { fprintf(stderr, "Failed to lock region 2 ")} //让程序休眠一分钟,用于测试 sleep(60)printf("Process %d closing file ", getpid())close(file_desc)exit(EXIT_SUCCESS)}
下面的源文件filelock3.c用于测试上一个文件设置的锁,测试可否对两个区域都加上一个读锁,代码如下:
#include <unistd.h>#include <stdlib.h>#include <stdio.h>#include <fcntl.h>int main() { const char *test_file = "test_lock.txt"int file_desc = -1int byte_count = 0char *byte_to_write = "A"struct flock region_1struct flock region_2int res = 0//打开数据文件 file_desc = open(test_file, O_RDWR|O_CREAT, 0666)if(!file_desc) { fprintf(stderr, "Unable to open %s for read/write ", test_file)exit(EXIT_FAILURE)} //设置区域1的锁类型 struct flock region_test1region_test1.l_type = F_RDLCKregion_test1.l_whence = SEEK_SETregion_test1.l_start = 10region_test1.l_len = 20region_test1.l_pid = -1//设置区域2的锁类型 struct flock region_test2region_test2.l_type = F_RDLCKregion_test2.l_whence = SEEK_SETregion_test2.l_start = 40region_test2.l_len = 10region_test2.l_pid = -1//
三、解空锁问题
如果我要给在本进程中没有加锁的区域解锁会发生什么事情呢?而如果这个区域中其他的进程有对其进行加锁又会发生什么情况呢?
如果一个进程实际并未对一个区域进行锁定,而调用解锁 *** 作也会成功,但是它并不能解其他的进程加在同一区域上的锁。也可以说解锁请求最终的结果取决于这个进程在文件中设置的任何锁,没有加锁,但对其进行解锁得到的还是没有加锁的状态。
进程间通信有一种[共享内存]方式,大家有没有想过,这种通信方式中如何解决数据竞争问题?我们可能自然而然的就会想到用锁。但我们平时使用的锁都是用于解决线程间数据竞争问题,貌似没有看到过它用在进程中,那怎么办?
关于进程间的通信方式估计大多数人都知道,这也是常见的面试八股文之一。
个人认为这种面试题没什么意义,无非就是答几个关键词而已,更深入的可能面试官和面试者都不太了解。
关于进程间通信方式我之前在【这篇文章】中有过介绍,感兴趣的可以移步去看哈。
进程间通信有一种[共享内存]方式,大家有没有想过,这种通信方式中如何解决数据竞争问题?
我们可能自然而然的就会想到用锁。但我们平时使用的锁都是用于解决线程间数据竞争问题,貌似没有看到过它用在进程中,那怎么办?
我找到了两种方法,信号量和互斥锁。
直接给大家贴代码吧,首先是信号量方式:
代码中的MEOW_DEFER,它内部的函数会在生命周期结束后触发。它的核心函数其实就是下面这四个:
具体含义大家应该看名字就知道,这里的重点就是sem_init中的pshared参数,该参数为1表示可在进程间共享,为0表示只在进程内部共享。
第二种方式是使用锁,即pthread_mutex_t,可是pthread_mutex不是用作线程间数据竞争的吗,怎么能用在进程间呢?
可以给它配置一个属性,示例代码如下:
它的默认属性是进程内私有,但是如果给它配置成PTHREAD_PROCESS_SHARED,它就可以用在进程间通信中。
相关视频推荐
360度无死角讲解进程管理,调度器的5种实现
Linux进程间通信-信号量、消息队列和共享内存
学习地址:C/C++Linux服务器开发/后台架构师【零声教育】-学习视频教程-腾讯课堂
需要C/C++ Linux服务器架构师学习资料加qun812855908获取(资料包括 C/C++,Linux,golang技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg 等),免费分享
完整代码如下:
我想这两种方式应该可以满足我们日常开发过程中的大多数需求。
锁的方式介绍完之后,可能很多朋友自然就会想到原子变量,这块我也搜索了一下。但是也不太确定C++标准中的atomic是否在进程间通信中有作用,不过看样子boost中的atomic是可以用在进程间通信中的。
其实在研究这个问题的过程中,还找到了一些很多解决办法,包括:
Disabling Interrupts
Lock Variables
Strict Alternation
Peterson's Solution
The TSL Instruction
Sleep and Wakeup
Semaphores
Mutexes
Monitors
Message Passing
Barriers
这里就不过多介绍啦,大家感兴趣的可以自行查阅资料哈。
Linux之父林纳斯·托瓦兹(Linus Torvalds)上周六宣布在新版Linux系统内核中首次加入锁定功能。
这项名为“lockdown”的Linux内核新安全功能将作为LSM(Linux安全模块)出现在即将发布的Linux 5.4版本当中。
该功能默认情况下处于关闭状态,由于存在破坏现有系统的风险,因此用户可选使用。这项新功能的主要目的是通过防止root帐户与内核代码进行交互来加强用户态进程与内核代码之间的鸿沟。
启用后,新的“锁定”功能将限制Linux某些内核功能,即使对于root用户也是如此,这使得受到破坏的root帐户更难于破坏其余的系统内核。
托瓦兹表示:“启用后,各种内核功能都受到限制。 ” 这包括限制对内核功能的访问,这些功能可能允许通过用户级进程提供的代码执行任意代码;阻止进程写入或读取/ dev / mem和/ dev / kmem内存;阻止对打开/ dev / port的访问,以防止原始端口访问;加强内核模块签名等。
Linux是一种自由和开放源码的类UNIX *** 作系统。该 *** 作系统的内核由林纳斯·托瓦兹在1991年10月5日首次发布。在加上用户空间的应用程序之后,成为 Linux *** 作系统。Linux也是最著名的自由软件和开放源代码软件。只要遵循GNU 通用公共许可证(GPL),任何个人和机构都可以自由地使用Linux 的所有底层源代码,也可以自由地修改和再发布。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)