i+在c语言中的意思,i+是什么集合

  

  1.1内存映射I/O内存映射I/O(memory-mapped I/O)是一种基于内存区域的高级I/O操作,可以将一个文件映射到进程地址空间中的一个内存区域。当从这个内存中读取数据时,相当于读取文件中的数据(读取文件),当向这个内存中写入数据时,相当于直接向文件中写入数据(是的这样就可以在不使用基本I/O操作函数read()和write()的情况下进行I/O操作。   

  

  I.1.1mmap()和munmap()函数为了实现内存映射I/O的功能,我们需要告诉内核将给定的文件映射到进程地址空间中的一个内存区域,这是通过系统调用mmap()来实现的。该函数的原型如下:   

  

  #include sys/mman.h   

  

  void *mmap(void *addr,size_t length,int prot,int flags,int fd,off _ t offset);   

  

  要使用这个函数,您需要包含头文件sys/mman.h   

  

  函数参数和返回值含义如下:   

  

  addr:参数addr用于指定映射到内存区域的起始地址。通常设置为NULL,表示映射区域的起始地址由系统选择,这是最常见的设置方法;如果参数addr不为空,说明你指定了映射区的起始地址,这个函数的返回值就是映射区的起始地址。   

  

  length:参数length指定了映射长度,它表示有多少文件被映射到内存区域。字节的单位,比如length=1024 * 4,表示文件的4K字节大小被映射到内存区域。   

  

  offset:映射的偏移量,通常设置为0,表示映射从文件头开始;于是参数offset和参数length决定了文件的起始位置和长度,并将这部分文件映射到内存区域,如图14.5.1所示。   

  

  fd:描述符,指定要映射到内存区域的文件。   

  

  prot:参数prot指定了映射区域的保护要求,其值如下:   

  

  l PROT_EXEC:映射区域是可执行的;   

  

  l PROT_READ:映射区是可读的;   

  

  l PROT_WRITE:测图区可以写;   

  

  l PROT_NONE:映射区域无法访问。   

  

  您可以将prot指定为PROT _无,或者将其设置为PROT _执行、PROT _读取和PROT _写入中的一个或多个(通过按位或运算符的任意组合)。指定映射区域的保护要求不能超过文件open()的访问权限。例如,如果文件以只读权限打开,则映射区域不能指定为PROT _写。   

  

  参数flags:标志会影响映射区域的许多属性。参数标志必须指定以下两个标志之一:   

  

  该l MAP_SHARED:标志指定当数据被写入映射区时,数据将被写入文件,也就是说,写入映射区的数据将被更新到文件并由其他进程共享。   

  

  此l MAP_PRIVATE:标志指定当数据写入映射区时,将创建映射文件的私有副本(写入时复制),并且映射区上的任何操作都不会更新到该文件,而只会读取和写入文件副本。   

  

  此外,可以通过按位or运算符将0个或多个以下标志组合成参数标志:   

  

  如果l MAP_FIXED:没有指定这个标志,如果参数addr不等于NULL,就意味着调用者指定了映射区的起始地址,但这只是一个建议,并不是强制,所以内核并不保证参数addr指定的值会作为映射区的起始地址;如果指定了MAP_FIXED标志,则意味着参数addr指定的值必须用作起始地址。如果不能用指定的值成功建立映射,放弃!通常不建议使用此标志,因为它不利于迁移。   

  

  当建立l MAP_ANONYMOUS:匿名映射时,参数fd和offset将被忽略,不涉及任何文件,并且映射区域不能与其他进程共享。   

  

  它与l MAP_ANON:地图匿名标志同义,不推荐使用。   

  

  l MAP_DENYWRITE:符号被忽略。   

  

  l MAP_EXECUTABLE:符号被忽略。   

  

  l MAP_FILE:兼容性标志,已被忽略。   

  

  锁定l MAP_LOCKED:映射区域。   

  

  除了上述迹象,还有其他迹象,   

这里便不再介绍,可通过man手册进行查看。在众多标志当中,通常情况下,参数flags中只指定了MAP_SHARED。

  

返回值:成功情况下,函数的返回值便是映射区的起始地址;发生错误时,返回(void *)-1,通常使用MAP_FAILED来表示,并且会设置errno来指示错误原因。

  


  

图 14.5.1 存储映射I/O示意图

  

对于mmap()函数,参数addr和offset在不为NULL和0的情况下,addr和offset的值通常被要求是系统页大小的整数倍,可通过sysconf()函数获取页大小,如下所示(以字节为单位):

  

sysconf(_SC_PAGE_SIZE)

  

  

sysconf(_SC_PAGESIZE)

  

虽然对addr和offset有这种限制,但对于参数length长度来说,却没有这种要求,如果映射区的长度不是页长度的整数倍时,会怎么样呢?对于这个问题的答案,我们首先需要了解到,对于mmap()函数来说,当文件成功被映射到内存区域时,这段内存区域(映射区)的大小通常是页大小的整数倍,即使参数length并不是页大小的整数倍。如果文件大小为96个字节,我们调用mmap()时参数length也是设置为96,假设系统页大小为4096字节(4K),则系统通常会提供4096个字节的映射区,其中后4000个字节会被设置为0,可以修改后面的这4000个字节,但是并不会影响到文件。但如果访问4000个字节后面的内存区域,将会导致异常情况发生,产生SIGBUS信号。

  

对于参数length任需要注意,参数length的值不能大于文件大小,即文件被映射的部分不能超出文件。

  

与映射区相关的两个信号

  

l SIGSEGV:如果映射区被mmap()指定成了只读的,那么进程试图将数据写入到该映射区时,将会产生SIGSEGV信号,此信号由内核发送给进程。在第八章中给大家介绍过该信号,该信号的系统默认操作是终止进程、并生成核心可用于调试的核心转储文件。

  

l SIGBUS:如果映射区的某个部分在访问时已不存在,则会产生SIGBUS信号。例如,调用mmap()进行映射时,将参数length设置为文件长度,但在访问映射区之前,另一个进程已将该文件截断(譬如调用ftruncate()函数进行截断),此时如果进程试图访问对应于该文件已截去部分的映射区,进程将会受到内核发送过来的SIGBUS信号,同样,该信号的系统默认操作是终止进程、并生成核心可用于调试的核心转储文件。

  

munmap()解除映射

  

通过open()打开文件,需要使用close()将将其关闭;同理,通过mmap()将文件映射到进程地址空间中的一块内存区域中,当不再需要时,必须解除映射,使用munmap()解除映射关系,其函数原型如下所示:

  

#include <sys/mman.h>

  


  

int munmap(void *addr, size_t length);

  

同样,使用该函数需要包含头文件<sys/mman.h>。

  

munmap()系统调用解除指定地址范围内的映射,参数addr指定待解除映射地址范围的起始地址,它必须是系统页大小的整数倍;参数length是一个非负整数,指定了待解除映射区域的大小(字节数),被解除映射的区域对应的大小也必须是系统页大小的整数倍,即使参数length并不等于系统页大小的整数倍,与mmap()函数相似。

  

需要注意的是,当进程终止时也会自动解除映射(如果程序中没有显式调用munmap()),但调用close()关闭文件时并不会解除映射。

  

通常将参数addr设置为mmap()函数的返回值,将参数length设置为mmap()函数的参数length,表示解除整个由mmap()函数所创建的映射。

  

使用示例

  

通过以上介绍,接下来我们编写一个简单地示例代码,使用存储映射I/O进行文件复制。

  

示例代码 14.5.1演示了使用存储映射I/O实现文件复制操作,将源文件中的内容全部复制到另一个目标文件中,其效果类似于cp命令。

  

示例代码 14.5.1 使用存储映射I/O复制文件

  

#include <stdio.h>

  

#include <stdlib.h>

  

#include <sys/types.h>

  

#include <sys/stat.h>

  

#include <fcntl.h>

  

#include <unistd.h>

  

#include <sys/mman.h>

  

#include <string.h>

  

int main(int argc, char *argv<>)

  

{

  

int srcfd, dstfd;

  

void *srcaddr;

  

void *dstaddr;

  

int ret;

  

struct stat sbuf;

  

if (3 != argc) {

  

fprintf(stderr, "usage: %s <srcfile> <dstfile>\n", argv<0>);

  

exit(-1);

  

}

  

/* 打开源文件 */

  

srcfd = open(argv<1>, O_RDONLY);

  

if (-1 == srcfd) {

  

perror("open error");

  

exit(-1);

  

}

  

/* 打开目标文件 */

  

dstfd = open(argv<2>, O_RDWR |

  

O_CREAT | O_TRUNC, 0664);

  

if (-1 == dstfd) {

  

perror("open error");

  

ret = -1;

  

goto out1;

  

}

  

/* 获取源文件的大小 */

  

fstat(srcfd, &sbuf);

  

/* 设置目标文件的大小 */

  

ftruncate(dstfd, sbuf.st_size);

  

/* 将源文件映射到内存区域中 */

  

srcaddr = mmap(NULL, sbuf.st_size,

  

PROT_READ, MAP_SHARED, srcfd, 0);

  

if (MAP_FAILED == srcaddr) {

  

perror("mmap error");

  

ret = -1;

  

goto out2;

  

}

  

/* 将目标文件映射到内存区域中 */

  

dstaddr = mmap(NULL, sbuf.st_size,

  

PROT_WRITE, MAP_SHARED, dstfd, 0);

  

if (MAP_FAILED == dstaddr) {

  

perror("mmap error");

  

ret = -1;

  

goto out3;

  

}

  

/* 将源文件中的内容复制到目标文件中 */

  

memcpy(dstaddr, srcaddr, sbuf.st_size);

  

/* 程序退出前清理工作 */

  

out4:

  

/* 解除目标文件映射 */

  

munmap(dstaddr, sbuf.st_size);

  

out3:

  

/* 解除源文件映射 */

  

munmap(srcaddr, sbuf.st_size);

  

out2:

  

/* 关闭目标文件 */

  

close(dstfd);

  

out1:

  

/* 关闭源文件并退出 */

  

close(srcfd);

  

exit(ret);

  

}

  

当执行程序的时候,将源文件和目标文件传递给应用程序,该程序首先会将源文件和目标文件打开,源文件以只读方式打开,而目标文件以可读、可写方式打开,如果目标文件不存在则创建它,并且将文件的大小截断为0。

  

然后使用fstat()函数获取源文件的大小,接着调用ftruncate()函数设置目标文件的大小与源文件大小保持一致。

  

然后对源文件和目标文件分别调用mmap(),将文件映射到内存当中;对于源文件,调用mmap()时将参数prot指定为PROT_READ,表示对它的映射区会进行读取操作;对于目标文件,调用mmap()时将参数port指定为PROT_WRITE,表示对它的映射区会进行写入操作。最后调用memcpy()将源文件映射区中的内容复制到目标文件映射区中,完成文件的复制操作。

  

接下来我们进行测试,笔者使用当前目录下的srcfile作为源文件,dstfile作为目标文件,先看看源文件srcfile的内容,如下所示:

  


  

图 14.5.2 源文件中的内容

  

目标文件dstfile并不存在,我们需要在程序中进行创建,编译程序、运行:

  


  

图 14.5.3 测试结果

  

由打印信息可知,程序运行完之后,生成了目标文件dstfile,使用cat命令查看到其内容与源文件srcfile相同,本测试程序成功实现了文件复制功能!

  

一.1.2 mprotect()函数使用系统调用mprotect()可以更改一个现有映射区的保护要求,其函数原型如下所示:

  

#include <sys/mman.h>

  


  

int mprotect(void *addr, size_t len, int prot);

  

使用该函数,同样需要包含头文件<sys/mman.h>。

  

参数prot的取值与mmap()函数的prot参数的一样,mprotect()函数会将指定地址范围的保护要求更改为参数prot所指定的类型,参数addr指定该地址范围的起始地址,addr的值必须是系统页大小的整数倍;参数len指定该地址范围的大小。

  

mprotect()函数调用成功返回0;失败将返回-1,并且会设置errno来只是错误原因。

  

一.1.3 msync()函数在第四章中提到过,read()和write()系统调用在操作磁盘文件时不会直接发起磁盘访问(读写磁盘硬件),而是仅仅在用户空间缓冲区和内核缓冲区之间复制数据,在后续的某个时刻,内核会将其缓冲区中的数据写入(刷新至)磁盘中,所以由此可知,调用write()写入到磁盘文件中的数据并不会立马写入磁盘,而是会先缓存在内核缓冲区中,所以就会出现write()操作与磁盘操作并不同步,也就是数据不同步。

  

对于存储I/O来说亦是如此,写入到文件映射区中的数据也不会立马刷新至磁盘设备中,而是会在我们将数据写入到映射区之后的某个时刻将映射区中的数据写入磁盘中。所以会导致映射区中的内容与磁盘文件中的内容不同步。我们可以调用msync()函数将映射区中的数据刷写、更新至磁盘文件中(同步操作),系统调用msync()类似于fsync()函数,不过msync()作用于映射区。该函数原型如下所示:

  

#include <sys/mman.h>

  


  

int msync(void *addr, size_t length, int flags);

  

使用该函数,同样需要包含头文件<sys/mman.h>。

  

参数addr和length指定了需同步的内存区域的起始地址和大小。对于参数addr来说,同样也要求必须是系统页大小的整数倍,也就是与系统页大小对齐。譬如,调用msync()时,将addr设置为mmap()函数的返回值,将length设置为mmap()函数的length参数,将对文件的整个映射区进行同步操作。

  

参数flags应指定为MS_ASYNC和MS_SYNC两个标志之一,除此之外,还可以根据需求选择是否指定MS_INVALIDATE标志,作为一个可选标志。

  

l MS_ASYNC:以异步方式进行同步操作。调用msync()函数之后,并不会等待数据完全写入磁盘之后才返回。

  

l MS_SYNC:以同步方式进行同步操作。调用msync()函数之后,需等待数据全部写入磁盘之后才返回。

  

l MS_INVALIDATE:是一个可选标志,请求使同一文件的其它映射无效(以便可以用刚写入的新值更新它们)。

  

msync()函数在调用成功情况下返回0;失败将返回-1、并设置errno。

  

munmap()函数并不影响被映射的文件,也就是说,当调用munmap()解除映射时并不会将映射区中的内容写到磁盘文件中。如果mmap()指定了MAP_SHARED标志,对于文件的更新,会在我们将数据写入到映射区之后的某个时刻将映射区中的数据更新到磁盘文件中,由内核根据虚拟存储算法自动进行。

  

如果mmap()指定了MAP_PRIVATE标志,在解除映射之后,进程对映射区的修改将会丢弃!

  

一.1.4 普通I/O与存储映射I/O比较通过前面的介绍,相信大家对存储映射I/O之间有了一个新的认识,本小节我们再来对普通I/O方式和存储映射I/O做一个简单的总结。

  

普通I/O方式的缺点

  

普通I/O方式一般是通过调用read()和write()函数来实现对文件的读写,使用read()和write()读写文件时,函数经过层层的调用后,才能够最终操作到文件,中间涉及到很多的函数调用过程,数据需要在不同的缓存间倒腾,效率会比较低。同样使用标准I/O(库函数fread()、fwrite())也是如此,本身标准I/O就是对普通I/O的一种封装。

  

那既然效率较低,为啥还要使用这种方式呢?原因在于,只有当数据量比较大时,效率的影响才会比较明显,如果数据量比较小,影响并不大,使用普通的I/O方式还是非常方便的。

  

存储映射I/O的优点

  

存储映射I/O的实质其实是共享,与IPC之内存共享很相似。譬如执行一个文件复制操作来说,对于普通I/O方式,首先需要将源文件中的数据读取出来存放在一个应用层缓冲区中,接着再将缓冲区中的数据写入到目标文件中,如下所示:

  


  

图 14.5.4 普通I/O实现文件复制示例图

  

而对于存储映射I/O来说,由于源文件和目标文件都已映射到了应用层的内存区域中,所以直接操作映射区来实现文件复制,如下所示:

  


  

图 14.5.5 存储映射I/O实现文件复制

  

首先非常直观的一点就是,使用存储映射I/O减少了数据的复制操作,所以在效率上会比普通I/O要高,其次上面也讲了,普通I/O中间涉及到了很多的函数调用过程,这些都会导致普通I/O在效率上会比存储映射I/O要低。

  

前面提到存储映射I/O的实质其实是共享,如何理解共享呢?其实非常简单,我们知道,应用层与内核层是不能直接进行交互的,必须要通过操作系统提供的系统调用或库函数来与内核进行数据交互,包括操作硬件。通过存储映射I/O将文件直接映射到应用程序地址空间中的一块内存区域中,也就是映射区;直接将磁盘文件直接与映射区关联起来,不用调用read()、write()系统调用,直接对映射区进行读写操作即可操作磁盘上的文件,而磁盘文件中的数据也可反应到映射区中,这就是一种共享,可以认为映射区就是应用层与内核层之间的共享内存。

  

存储映射I/O的不足

  

存储映射I/O方式并不是完美的,它所映射的文件只能是固定大小,因为文件所映射的区域已经在调用mmap()函数时通过length参数指定了。另外,文件映射的内存区域的大小必须是系统页大小的整数倍,譬如映射文件的大小为96字节,假定系统页大小为4096字节,那么剩余的4000字节全部填充为0,虽然可以通过映射地址访问剩余的这些字节数据,但不能在映射文件中反应出来,由此可知,使用存储映射I/O在进行大数据量操作时比较有效;对于少量数据,使用普通I/O方式更加方便!

  

存储映射I/O的应用场景

  

由上面介绍可知,存储映射I/O在处理大量数据时效率高,对于少量数据处理不是很划算,所以通常来说,存储映射I/O会在视频图像处理方面用的比较多,譬如在第二篇内容,我们将会介绍Framebuffer编程,通俗点说就是LCD编程,就会使用到存储映射I/O。

  

一.2 文件锁现象一下,当两个人同时编辑磁盘中同一份文件时,其后果将会如何呢?在Linux系统中,该文件的最后状态通常取决于写该文件的最后一个进程。多个进程同时操作同一文件,很容易导致文件中的数据发生混乱,因为多个进程对文件进行I/O操作时,容易产生竞争状态、导致文件中的内容与预想的不一致!

  

对于有些应用程序,进程有时需要确保只有它自己能够对某一文件进行I/O操作,在这段时间内不允许其它进程对该文件进行I/O操作。为了向进程提供这种功能,Linux系统提供了文件锁机制。

  

前面学习过互斥锁、自旋锁以及读写锁,文件锁与这些锁一样,都是内核提供的锁机制,锁机制实现用于对共享资源的访问进行保护;只不过互斥锁、自旋锁、读写锁与文件锁的应用场景不一样,互斥锁、自旋锁、读写锁主要用在多线程环境下,对共享资源的访问进行保护,做到线程同步。

  

而文件锁,顾名思义是一种应用于文件的锁机制,当多个进程同时操作同一文件时,我们怎么保证文件数据的正确性,linux通常采用的方法是对文件上锁,来避免多个进程同时操作同一文件时产生竞争状态。譬如进程对文件进行I/O操作时,首先对文件进行上锁,将其锁住,然后再进行读写操作;只要进程没有对文件进行解锁,那么其它的进程将无法对其进行操作;这样就可以保证,文件被锁住期间,只有它(该进程)可以对其进行读写操作。

  

一个文件既然可以被多个进程同时操作,那说明文件必然是一种共享资源,所以由此可知,归根结底,文件锁也是一种用于对共享资源的访问进行保护的机制,通过对文件上锁,来避免访问共享资源产生竞争状态。

  

文件锁的分类

  

文件锁可以分为建议性锁和强制性锁两种:

  

l 建议性锁

  

建议性锁本质上是一种协议,程序访问文件之前,先对文件上锁,上锁成功之后再访问文件,这是建议性锁的一种用法;但是如果你的程序不管三七二十一,在没有对文件上锁的情况下直接访问文件,也是可以访问的,并非无法访问文件;如果是这样,那么建议性锁就没有起到任何作用,如果要使得建议性锁起作用,那么大家就要遵守协议,访问文件之前先对文件上锁。这就好比交通信号灯,规定红灯不能通行,绿灯才可以通行,但如果你非要在红灯的时候通行,谁也拦不住你,那么后果将会导致发生交通事故;所以必须要大家共同遵守交通规则,交通信号灯才能起到作用。

  

l 强制性锁:

  

强制性锁比较好理解,它是一种强制性的要求,如果进程对文件上了强制性锁,其它的进程在没有获取到文件锁的情况下是无法对文件进行访问的。其本质原因在于,强制性锁会让内核检查每一个I/O操作(譬如read()、write()),验证调用进程是否是该文件锁的拥有者,如果不是将无法访问文件。当一个文件被上锁进行写入操作的时候,内核将阻止其它进程对其进行读写操作。采取强制性锁对性能的影响很大,每次进行读写操作都必须检查文件锁。

  

在Linux系统中,可以调用flock()、fcntl()以及lockf()这三个函数对文件上锁,接下来将向大家介绍每个函数的使用方法。

  

一.2.1 flock()函数加锁先来学习系统调用flock(),使用该函数可以对文件加锁或者解锁,但是flock()函数只能产生建议性锁,其函数原型如下所示:

  

#include <sys/file.h>

  


  

int flock(int fd, int operation);

  

使用该函数需要包含头文件<sys/file.h>。

  

函数参数和返回值含义如下:

  

fd:参数fd为文件描述符,指定需要加锁的文件。

  

operation:参数operation指定了操作方式,可以设置为以下值的其中一个:

  

l LOCK_SH:在fd引用的文件上放置一把共享锁。所谓共享,指的便是多个进程可以拥有对同一个文件的共享锁,该共享锁可被多个进程同时拥有。

  

l LOCK_EX:在fd引用的文件上放置一把排它锁(或叫互斥锁)。所谓互斥,指的便是互斥锁只能同时被一个进程所拥有。

  

l LOCK_UN:解除文件锁定状态,解锁、释放锁。

  

除了以上三个标志外,还有一个标志:

  

l LOCK_NB:表示以非阻塞方式获取锁。默认情况下,调用flock()无法获取到文件锁时会阻塞、直到其它进程释放锁为止,如果不想让程序被阻塞,可以指定LOCK_NB标志,如果无法获取到锁应立刻返回(错误返回,并将errno设置为EWOULDBLOCK),通常与LOCK_SH或LOCK_EX一起使用,通过位或运算符组合在一起。

  

返回值:成功将返回0;失败返回-1、并会设置errno,

  

对于flock(),需要注意的是,同一个文件不会同时具有共享锁和互斥锁。

  

使用示例

  

示例代码 14.6.1演示了使用flock()函数对一个文件加锁和解锁(建议性锁)。程序首先调用open()函数将文件打开,文件路径通过传参的方式传递进来;文件打开成功之后,调用flock()函数对文件加锁(非阻塞方式、排它锁),并打印出“文件加锁成功”信息,如果加锁失败便会打印出“文件加锁失败”信息。然后调用signal函数为SIGINT信号注册了一个信号处理函数,当进程接收到SIGINT信号后会执行sigint_handler()函数,在信号处理函数中对文件进行解锁,然后终止进程。

  

示例代码 14.6.1 使用flock()对文件加锁/解锁

  

#include <stdio.h>

  

#include <stdlib.h>

  

#include <sys/types.h>

  

#include <sys/stat.h>

  

#include <fcntl.h>

  

#include <unistd.h>

  

#include <sys/file.h>

  

#include <signal.h>

  

static int fd = -1; //文件描述符

  

/* 信号处理函数 */

  

static void sigint_handler(int sig)

  

{

  

if (SIGINT != sig)

  

return;

  

/* 解锁 */

  

flock(fd, LOCK_UN);

  

close(fd);

  

printf("进程1: 文件已解锁!\n");

  

}

  

int main(int argc, char *argv<>)

  

{

  

if (2 != argc) {

  

fprintf(stderr, "usage: %s <file>\n", argv<0>);

  

exit(-1);

  

}

  

/* 打开文件 */

  

fd = open(argv<1>, O_WRONLY);

  

if (-1 == fd) {

  

perror("open error");

  

exit(-1);

  

}

  

/* 以非阻塞方式对文件加锁(排它锁) */

  

if (-1 == flock(fd, LOCK_EX | LOCK_NB)) {

  

perror("进程1: 文件加锁失败");

  

exit(-1);

  

}

  

printf("进程1: 文件加锁成功!\n");

  

/* 为SIGINT信号注册处理函数 */

  

signal(SIGINT, sigint_handler);

  

for ( ; ; )

  

sleep(1);

  

}

  

加锁成功之后,程序进入了for死循环,一直持有锁;此时我们可以执行另一个程序,如示例代码 14.6.2所示,该程序首先也会打开文件,文件路径通过传参的方式传递进来,同样在程序中也会调用flock()函数对文件加锁(排它锁、非阻塞方式),不管加锁成功与否都会执行下面的I/O操作,将数据写入文件、在读取出来并打印。

  

示例代码 14.6.2 未获取锁情况下读写文件

  

#include <stdio.h>

  

#include <stdlib.h>

  

#include <sys/types.h>

  

#include <sys/stat.h>

  

#include <fcntl.h>

  

#include <unistd.h>

  

#include <sys/file.h>

  

#include <string.h>

  

int main(int argc, char *argv<>)

  

{

  

char buf<100> = "Hello World!";

  

int fd;

  

int len;

  

if (2 != argc) {

  

fprintf(stderr, "usage: %s <file>\n", argv<0>);

  

exit(-1);

  

}

  

/* 打开文件 */

  

fd = open(argv<1>, O_RDWR);

  

if (-1 == fd) {

  

perror("open error");

  

exit(-1);

  

}

  

/* 以非阻塞方式对文件加锁(排它锁) */

  

if (-1 == flock(fd, LOCK_EX | LOCK_NB))

  

perror("进程2: 文件加锁失败");

  

else

  

printf("进程2: 文件加锁成功!\n");

  

/* 写文件 */

  

len = strlen(buf);

  

if (0 > write(fd, buf, len)) {

  

perror("write error");

  

exit(-1);

  

}

  

printf("进程2: 写入到文件的字符串<%s>\n", buf);

  

/* 将文件读写位置移动到文件头 */

  

if (0 > lseek(fd, 0x0, SEEK_SET)) {

  

perror("lseek error");

  

exit(-1);

  

}

  

/* 读文件 */

  

memset(buf, 0x0, sizeof(buf)); //清理buf

  

if (0 > read(fd, buf, len)) {

  

perror("read error");

  

exit(-1);

  

}

  

printf("进程2: 从文件读取的字符串<%s>\n", buf);

  

/* 解锁、退出 */

  

flock(fd, LOCK_UN);

  

close(fd);

  

exit(0);

  

}

  

把示例代码 14.6.1作为应用程序1,把示例代码 14.6.2作为应用程序2,将它们分别编译成不同的可执行文件testApp1和testApp2,如下所示:

  


  

图 14.6.1 两份可执行文件

  

在进行测试之前,创建一个测试用的文件infile,直接使用touch命令创建即可,首先执行testApp1应用程序,将infile文件作为输入文件,并将其放置在后台运行:

  


  

图 14.6.2 执行testApp1

  

testApp1会在后台运行,由ps命令可查看到其pid为20710。接着执行testApp2应用程序,传入相同的文件infile,如下所示:

  


  

图 14.6.3 执行testApp2

  

从打印信息可知,testApp2进程对infile文件加锁失败,原因在于锁已经被testApp1进程所持有,所以testApp2加锁自然会失败;但是可以发现虽然加锁失败,但是testApp2对文件的读写操作是没有问题的,是成功的,这就是建议性锁的特点;正确的使用方式是,在加锁失败之后不要再对文件进行I/O操作了,遵循这个协议。

  

接着我们向testApp1进程发送一个SIGIO信号,让其对文件infile解锁,接着再执行一次testApp2,如下所示:

  


  

图 14.6.4 测试结果

  

使用kill命令向testApp1进程发送编号为2的信号,也就是SIGIO信号,testApp1接收到信号之后,对infile文件进行解锁、然后退出;接着再次执行testApp2程序,从打印信息可知,这次能够成功对infile文件加锁了,读写也是没有问题的。

  

关于flock()的几条规则

  

l 同一进程对文件多次加锁不会导致死锁。当进程调用flock()对文件加锁成功,再次调用flock()对文件(同一文件描述符)加锁,这样不会导致死锁,新加的锁会替换旧的锁。譬如调用flock()对文件加共享锁,再次调用flock()对文件加排它锁,最终文件锁会由共享锁替换为排它锁。

  

l 文件关闭的时候,会自动解锁。进程调用flock()对文件加锁,如果在未解锁之前将文件关闭,则会导致文件锁自动解锁,也就是说,文件锁会在相应的文件描述符被关闭之后自动释放。同理,当一个进程终止时,它所建立的锁将全部释放。

  

l 一个进程不可以对另一个进程持有的文件锁进行解锁。

  

l 由fork()创建的子进程不会继承父进程所创建的锁。这意味着,若一个进程对文件加锁成功,然后该进程调用fork()创建了子进程,那么对父进程创建的锁而言,子进程被视为另一个进程,虽然子进程从父进程继承了其文件描述符,但不能继承文件锁。这个约束是有道理的,因为锁的作用就是阻止多个进程同时写同一个文件,如果子进程通过fork()继承了父进程的锁,则父进程和子进程就可以同时写同一个文件了。

  

除此之外,当一个文件描述符被复制时(譬如使用dup()、dup2()或fcntl()F_DUPFD操作),这些通过复制得到的文件描述符和源文件描述符都会引用同一个文件锁,使用这些文件描述符中的任何一个进行解锁都可以,如下所示:

  

flock(fd, LOCK_EX); //加锁

  

new_fd = dup(fd);

  

flock(new_fd, LOCK_UN); //解锁

  

这段代码先在fd上设置一个排它锁,然后使用dup()对fd进行复制得到新文件描述符new_fd,最后通过new_fd来解锁,这样可以解锁成功。但是,如果不显示的调用一个解锁操作,只有当所有文件描述符都被关闭之后锁才会被释放。譬如上面的例子中,如果不调用flock(new_fd, LOCK_UN)进行解锁,只有当fd和new_fd都被关闭之后锁才会自动释放。

  

关于本小节内容就暂时到这里为止!接下来我们将学习使用fcntl()对文件上锁。

  

一.2.2 fcntl()函数加锁fcntl()函数在前面章节内容中已经多次用到了,它是一个多功能文件描述符管理工具箱,通过配合不同的cmd操作命令来实现不同的功能。为了方便述说,这里再重申一次:

  

#include <unistd.h>

  

#include <fcntl.h>

  

int fcntl(int fd, int cmd, ... /* struct flock *flockptr */ );

  

与锁相关的cmd为F_SETLK、F_SETLKW、F_GETLK,第三个参数flockptr是一个struct flock结构体指针。使用fcntl()实现文件锁功能与flock()有两个比较大的区别:

  

l flock()仅支持对整个文件进行加锁/解锁;而fcntl()可以对文件的某个区域(某部分内容)进行加锁/解锁,可以精确到某一个字节数据。

  

l flock()仅支持建议性锁类型;而fcntl()可支持建议性锁和强制性锁两种类型。

  

我们先来看看struct flock结构体,如下所示:

  

示例代码 14.6.3 struct flock结构体

  

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(set by F_GETLK and F_OFD_GETLK) */

  

...

  

};

  

对struct flock结构体说明如下:

  

l l_type:所希望的锁类型,可以设置为F_RDLCK、F_WRLCK和F_UNLCK三种类型之一,F_RDLCK表示共享性质的读锁,F_WRLCK表示独占性质的写锁,F_UNLCK表示解锁一个区域。

  

l l_whence和l_start:这两个变量用于指定要加锁或解锁区域的起始字节偏移量,与2.7小节所学的lseek()函数中的offset和whence参数相同,这里不再重述,如果忘记了,可以回到2.7小节再看看。

  

l l_len:需要加锁或解锁区域的字节长度。

  

l l_pid:一个pid,指向一个进程,表示该进程持有的锁能阻塞当前进程,当cmd=F_GETLK时有效。

  

以上便是对struct flock结构体各成员变量的简单介绍,对于加锁和解锁区域的说明,还需要注意以下几项规则:

  

l 锁区域可以在当前文件末尾处开始或者越过末尾处开始,但是不能在文件起始位置之前开始。

  

l 若参数l_len设置为0,表示将锁区域扩大到最大范围,也就是说从锁区域的起始位置开始,到文件的最大偏移量处(也就是文件末尾)都处于锁区域范围内。而且是动态的,这意味着不管向该文件追加写了多少数据,它们都处于锁区域范围,起始位置可以是文件的任意位置。

  

l 如果我们需要对整个文件加锁,可以将l_whence和l_start设置为指向文件的起始位置,并且指定参数l_len等于0。

  

两种类型的锁:F_RDLCK和F_WRLCK

  

上面我们提到了两种类型的锁,分别为共享性读锁(F_RDLCK)和独占性写锁(F_WRLCK)。基本的规则与13.5小节所介绍的线程同步读写锁很相似,任意多个进程在一个给定的字节上可以有一把共享的读锁,但是在一个给定的字节上只能有一个进程有一把独占写锁,进一步而言,如果在一个给定的字节上已经有一把或多把读锁,则不能在该字节上加写锁;如果在一个字节上已经有一把独占性写锁,则不能再对它加任何锁(包括读锁和写锁),下图显示了这些兼容性规则:

  


  

图 14.6.5 不同类型锁彼此之间的兼容性

  

如果一个进程对文件的某个区域已经上了一把锁,后来该进程又试图在该区域再加一把锁,那么通常新加的锁将替换旧的锁。譬如,若某一进程在文件的100~200字节区间有一把写锁,然后又试图在100~200字节区间再加一把读锁,那么该请求将会成功执行,原来的写锁会替换为读锁。

  

还需要注意另外一个问题,当对文件的某一区域加读锁时,调用进程必须对该文件有读权限,譬如open()时flags参数指定了O_RDONLY或O_RDWR;当对文件的某一区域加写锁时,调用进程必须对该文件有写权限,譬如open()时flags参数指定了O_WRONLY或O_RDWR。

  

F_SETLK、F_SETLKW和F_GETLK

  

我们来看看与文件锁相关的三个cmd它们的作用:

  

l F_GETLK:这种用法一般用于测试,测试调用进程对文件加一把由参数flockptr指向的struct flock对象所描述的锁是否会加锁成功。如果加锁不成功,意味着该文件的这部分区域已经存在一把锁,并且由另一进程所持有,并且调用进程加的锁与现有锁之间存在排斥关系,现有锁会阻止调用进程想要加的锁,并且现有锁的信息将会重写参数flockptr指向的对象信息。如果不存在这种情况,也就是说flockptr指向的struct flock对象所描述的锁会加锁成功,则除了将struct flock对象的l_type修改为F_UNLCK之外,结构体中的其它信息保持不变。

  

l F_SETLK:对文件添加由flockptr指向的struct flock对象所描述的锁。譬如试图对文件的某一区域加读锁(l_type等于F_RDLCK)或写锁(l_type等于F_WRLCK),如果加锁失败,那么fcntl()将立即出错返回,此时将errno设置为EACCES或EAGAIN。也可用于清除由flockptr指向的struct flock对象所描述的锁(l_type等于F_UNLCK)。

  

l F_SETLKW:此命令是F_SETLK的阻塞版本(命令名中的W表示等待wait),如果所请求的读锁或写锁因另一个进程当前已经对所请求区域的某部分进行了加锁,而导致请求失败,那么调用进程将会进入阻塞状态。只有当请求的锁可用时,进程才会被唤醒。

  

F_GETLK命令一般很少用,事先用F_GETLK命令测试是否能够对文件加锁,然后再用F_SETLK或F_SETLKW命令对文件加锁,但这两者并不是原子操作,所以即使测试结果表明可以加锁成功,但是在使用F_SETLK或F_SETLKW命令对文件加锁之前也有可能被其它进程锁住。

  

使用示例与测试

  

示例代码 14.6.4演示了使用fcntl()对文件加锁和解锁的操作。需要加锁的文件通过外部传参传入,先调用open()函数以只写方式打开文件;接着对struct flock类型对象lock进行填充,l_type设置为F_WRLCK表示加一个写锁,通过l_whence和l_start两个变量将加锁区域的起始位置设置为文件头部,接着将l_len设置为0表示对整个文件加锁。

  

示例代码 14.6.4 使用fcntl()对文件加锁/解锁使用示例

  

#include <stdio.h>

  

#include <stdlib.h>

  

#include <sys/types.h>

  

#include <sys/stat.h>

  

#include <fcntl.h>

  

#include <unistd.h>

  

#include <string.h>

  

int main(int argc, char *argv<>)

  

{

  

struct flock lock = {0};

  

int fd = -1;

  

char buf<> = "Hello World!";

  

/* 校验传参 */

  

if (2 != argc) {

  

fprintf(stderr, "usage: %s <file>\n", argv<0>);

  

exit(-1);

  

}

  

/* 打开文件 */

  

fd = open(argv<1>, O_WRONLY);

  

if (-1 == fd) {

  

perror("open error");

  

exit(-1);

  

}

  

/* 对文件加锁 */

  

lock.l_type = F_WRLCK; //独占性写锁

  

lock.l_whence = SEEK_SET; //文件头部

  

lock.l_start = 0; //偏移量为0

  

lock.l_len = 0;

  

if (-1 == fcntl(fd, F_SETLK, &lock)) {

  

perror("加锁失败");

  

exit(-1);

  

}

  

printf("对文件加锁成功!\n");

  

/* 对文件进行写操作 */

  

if (0 > write(fd, buf, strlen(buf))) {

  

perror("write error");

  

exit(-1);

  

}

  

/* 解锁 */

  

lock.l_type = F_UNLCK; //解锁

  

fcntl(fd, F_SETLK, &lock);

  

/* 退出 */

  

close(fd);

  

exit(0);

  

}

  

整个代码很简单,比较容易理解,具体执行的结果就不再给大家演示了。

  

一个进程可以对同一个文件的不同区域进行加锁,当然这两个区域不能有重叠的情况。示例代码 14.6.5演示了一个进程对同一文件的两个不同区域分别加读锁和写锁,对文件的100~200字节区间加了一个写锁,对文件的400~500字节区间加了一个读锁。

  

示例代码 14.6.5 对文件的不同区域进行加锁

  

#include <stdio.h>

  

#include <stdlib.h>

  

#include <sys/types.h>

  

#include <sys/stat.h>

  

#include <fcntl.h>

  

#include <unistd.h>

  

int main(int argc, char *argv<>)

  

{

  

struct flock wr_lock = {0};

  

struct flock rd_lock = {0};

  

int fd = -1;

  

/* 校验传参 */

  

if (2 != argc) {

  

fprintf(stderr, "usage: %s <file>\n", argv<0>);

  

exit(-1);

  

}

  

/* 打开文件 */

  

fd = open(argv<1>, O_RDWR);

  

if (-1 == fd) {

  

perror("open error");

  

exit(-1);

  

}

  

/* 将文件大小截断为1024字节 */

  

ftruncate(fd, 1024);

  

/* 对100~200字节区间加写锁 */

  

wr_lock.l_type = F_WRLCK;

  

wr_lock.l_whence = SEEK_SET;

  

wr_lock.l_start = 100;

  

wr_lock.l_len = 100;

  

if (-1 == fcntl(fd, F_SETLK, &wr_lock)) {

  

perror("加写锁失败");

  

exit(-1);

  

}

  

printf("加写锁成功!\n");

  

/* 对400~500字节区间加读锁 */

  

rd_lock.l_type = F_RDLCK;

  

rd_lock.l_whence = SEEK_SET;

  

rd_lock.l_start = 400;

  

rd_lock.l_len = 100;

  

if (-1 == fcntl(fd, F_SETLK, &rd_lock)) {

  

perror("加读锁失败");

  

exit(-1);

  

}

  

printf("加读锁成功!\n");

  

/* 对文件进行I/O操作 */

  

// ......

  

// ......

  

/* 解锁 */

  

wr_lock.l_type = F_UNLCK; //写锁解锁

  

fcntl(fd, F_SETLK, &wr_lock);

  

rd_lock.l_type = F_UNLCK; //读锁解锁

  

fcntl(fd, F_SETLK, &rd_lock);

  

/* 退出 */

  

close(fd);

  

exit(0);

  

}

  

如果两个区域出现了重叠,譬如100~200字节区间和150~250字节区间,150~200就是它们的重叠部分,一个进程对同一文件的相同区域不可能同时加两把锁,新加的锁会把旧的锁替换掉,譬如先对100~200字节区间加写锁、再对150~250字节区间加读锁,那么150~200字节区间最终是读锁控制的,关于这个问题,大家可以自己去验证、测试。

  

接下来对读锁和写锁彼此之间的兼容性进行测试,使用示例代码 14.6.6测试读锁的共享性。

  

示例代码 14.6.6 读锁的共享性测试

  

#include <stdio.h>

  

#include <stdlib.h>

  

#include <sys/types.h>

  

#include <sys/stat.h>

  

#include <fcntl.h>

  

#include <unistd.h>

  

int main(int argc, char *argv<>)

  

{

  

struct flock lock = {0};

  

int fd = -1;

  

/* 校验传参 */

  

if (2 != argc) {

  

fprintf(stderr, "usage: %s <file>\n", argv<0>);

  

exit(-1);

  

}

  

/* 打开文件 */

  

fd = open(argv<1>, O_RDWR);

  

if (-1 == fd) {

  

perror("open error");

  

exit(-1);

  

}

  

/* 将文件大小截断为1024字节 */

  

ftruncate(fd, 1024);

  

/* 对400~500字节区间加读锁 */

  

lock.l_type = F_RDLCK;

  

lock.l_whence = SEEK_SET;

  

lock.l_start = 400;

  

lock.l_len = 100;

  

if (-1 == fcntl(fd, F_SETLK, &lock)) {

  

perror("加读锁失败");

  

exit(-1);

  

}

  

printf("加读锁成功!\n");

  

for ( ; ; )

  

sleep(1);

  

}

  

首先运行上述示例代码,程序加读锁之后会进入死循环,进程一直在运行着、持有读锁。接着多次运行上述示例代码,启动多个进程加读锁,测试结果如下所示:

  


  

图 14.6.6 读锁共享性测试

  

从打印信息可以发现,多个进程对同一文件的相同区域都可以加读锁,说明读锁是共享性的。由于程序是放置在后台运行的,测试完毕之后,可以使用kill命令将这些进程杀死,或者直接关闭当前终端,重新启动新的终端。

  

使用示例代码 14.6.7测试写锁的独占性。

  

示例代码 14.6.7 写锁的独占性测试

  

#include <stdio.h>

  

#include <stdlib.h>

  

#include <sys/types.h>

  

#include <sys/stat.h>

  

#include <fcntl.h>

  

#include <unistd.h>

  

int main(int argc, char *argv<>)

  

{

  

struct flock lock = {0};

  

int fd = -1;

  

/* 校验传参 */

  

if (2 != argc) {

  

fprintf(stderr, "usage: %s <file>\n", argv<0>);

  

exit(-1);

  

}

  

/* 打开文件 */

  

fd = open(argv<1>, O_RDWR);

  

if (-1 == fd) {

  

perror("open error");

  

exit(-1);

  

}

  

/* 将文件大小截断为1024字节 */

  

ftruncate(fd, 1024);

  

/* 对400~500字节区间加写锁 */

  

lock.l_type = F_WRLCK;

  

lock.l_whence = SEEK_SET;

  

lock.l_start = 400;

  

lock.l_len = 100;

  

if (-1 == fcntl(fd, F_SETLK, &lock)) {

  

perror("加写锁失败");

  

exit(-1);

  

}

  

printf("加写锁成功!\n");

  

for ( ; ; )

  

sleep(1);

  

}

  

测试方法与读锁测试方法一样,如下所示:

  


  

图 14.6.7 写锁的独占性测试

  

由打印信息可知,但第一次启动的进程对文件加写锁之后,后面再启动进程对同一文件的相同区域加写锁发现都会失败,所以由此可知,写锁是独占性的。

  

几条规则

  

关于使用fcntl()创建锁的几条规则与flock()相似,如下所示:

  

l 文件关闭的时候,会自动解锁。

  

l 一个进程不可以对另一个进程持有的文件锁进行解锁。

  

l 由fork()创建的子进程不会继承父进程所创建的锁。

  

除此之外,当一个文件描述符被复制时(譬如使用dup()、dup2()或fcntl()F_DUPFD操作),这些通过复制得到的文件描述符和源文件描述符都会引用同一个文件锁,使用这些文件描述符中的任何一个进行解锁都可以,这点与flock()是一样的,如下所示:

  

lock.l_type = F_RDLCK;

  

fcntl(fd, F_SETLK, &lock);//加锁

  

new_fd = dup(fd);

  

lock.l_type = F_UNLCK;

  

fcntl(new_fd, F_SETLK, &lock);//解锁

  

这段代码先在fd上设置一个读锁,然后使用dup()对fd进行复制得到新文件描述符new_fd,最后通过new_fd来解锁,这样可以解锁成功。如果不显示的调用一个解锁操作,任何一个文件描述符被关闭之后锁都会自动释放,那么这点与flock()是不同的。譬如上面的例子中,如果不调用flock(new_fd, LOCK_UN)进行解锁,当fd或new_fd两个文件描述符中的任何一个被关闭之后锁都会自动释放。

  

建议性锁和强制性锁

  

前面我们提到了fcntl()支持强制性锁和建议性锁,但是一般不建议使用强制性锁,所以大部分情况下使用的都是建议性锁,那如何使能强制性锁呢?

  

对于一个特定的文件,开启它的强制性锁机制其实非常简单,主要跟文件的权限位有关系,在5.5小节对文件的权限进行了比较详细的介绍,这里不再重述!如果要开启强制性锁机制,需要设置文件的Set-Group-ID(S_ISGID)位为1,并且禁止文件的组用户执行权限(S_IXGRP),也就是将其设置为0。

  

但是,有些Linux/Unix发行版系统并不支持强制性锁机制,可以通过示例代码 14.6.8进行测试。

  

示例代码 14.6.8 测试系统是否支持强制性锁机制

  

#include <stdio.h>

  

#include <stdlib.h>

  

#include <sys/types.h>

  

#include <sys/stat.h>

  

#include <fcntl.h>

  

#include <unistd.h>

  

#include <sys/wait.h>

  

int main(int argc, char *argv<>)

  

{

  

struct stat sbuf = {0};

  

int fd = -1;

  

pid_t pid;

  

/* 校验传参 */

  

if (2 != argc) {

  

fprintf(stderr, "usage: %s <file>\n", argv<0>);

  

exit(-1);

  

}

  

/* 打开文件 */

  

fd = open(argv<1>, O_RDWR | O_CREAT | O_TRUNC, 0664);

  

if (-1 == fd) {

  

perror("open error");

  

exit(-1);

  

}

  

/* 写入一行字符串 */

  

if (12 != write(fd, "Hello World!", 12)) {

  

perror("write error");

  

exit(-1);

  

}

  

/* 开启强制性锁机制 */

  

if (0 > fstat(fd, &sbuf)) {//获取文件属性

  

perror("fstat error");

  

exit(-1);

  

}

  

if (0 > fchmod(fd, (sbuf.st_mode & ~S_IXGRP)

  

| S_ISGID)) {

  

perror("fchmod error");

  

exit(-1);

  

}

  

/* fork创建子进程 */

  

if (0 > (pid = fork())) //出错

  

perror("fork error");

  

else if (0 < pid) { //父进程

  

struct flock lock = {0};

  

/* 对整个文件加写锁 */

  

lock.l_type = F_WRLCK;

  

lock.l_whence = SEEK_SET;

  

lock.l_start = 0;

  

lock.l_len = 0;

  

if (0 > fcntl(fd, F_SETLK, &lock))

  

perror("父进程: 加写锁失败");

  

else

  

printf("父进程: 加写锁成功!\n");

  

printf("~~~~~~~~~~~~~~~~~~~\n");

  

if (0 > wait(NULL))

  

perror("wait error");

  

}

  

else { //子进程

  

struct flock lock = {0};

  

int flag;

  

char buf<20> = {0};

  

sleep(1); //休眠1秒钟,让父进程先运行

  

/* 设置为非阻塞方式 */

  

flag = fcntl(fd, F_GETFL);

  

flag |= O_NONBLOCK;

  

fcntl(fd, F_SETFL, flag);

  

/* 对整个文件加读锁 */

  

lock.l_type = F_RDLCK;

  

lock.l_whence = SEEK_SET;

  

lock.l_start = 0;

  

lock.l_len = 0;

  

if (-1 == fcntl(fd, F_SETLK, &lock))

  

perror("子进程: 加读锁失败");

  

else

  

printf("子进程: 加读锁成功!\n");

  

/* 读文件 */

  

if (0 > lseek(fd, 0, SEEK_SET))

  

perror("lseek error");

  

if (0 > read(fd, buf, 12))

  

perror("子进程: read error");

  

else

  

printf("子进程: read OK, buf = %s\n", buf);

  

}

  

exit(0);

  

}

  

此程序首先创建了一个文件,文件路径通过传参的方式传递给应用程序,如果不存在该文件则创建它。接着向文件中写入数据,开启文件的强制性锁机制。接下来程序调用fork()创建了一个子进程,在父进程分支中,对文件的所有区域加了一把独占性质的写锁,接着调用wait()等到回收子进程;在子进程分支中先是休眠了一秒钟以保证父进程先执行,子进程将文件设置为非阻塞方式,这里大家可能会有疑问?普通文件不都是非阻塞的吗?这里为什么要设置非阻塞呢?并不是多此一举,原因在于这里涉及到了强制性锁的问题,在强制性锁机制下,如果文件被进程添加了强制性写锁,其它进程读或写该文件将会被阻塞,所以我们需要显式设置为非阻塞方式。

  

设置为非阻塞之后,子进程试图对文件设置一把读锁,接着子进程将文件读、写位置移动到文件头,并试图read读该文件。

  

由于父进程已经对文件设置了写锁,子进程试图对文件设置读锁时,将会失败;子进程在没有获取到读锁的情况下,调用read()读取文件将会出现两种情况:如果系统支持强制性锁机制,那么read()将会失败;如果系统不支持强制性锁机制,read()将会成功!

  

接下来我们进行测试:

  


  

图 14.6.8 Ubuntu系统下测试结果

  

从打印信息可以发现,父进程设置了写锁的情况下,子进程再次对其设置读锁是不成功的,也就是子进程没有获取到读锁,但是读文件却是成功的,由此可知,我们测试所使用的Ubuntu系统不支持强制性锁机制。

  

一.2.3 lockf()函数加锁lockf()函数是一个库函数,其内部是基于fcntl()来实现的,所以lockf()是对fcntl锁的一种封装,具体的使用方法这里便不再介绍。

  

一.3 小结本章向大家介绍了几种高级I/O功能,非阻塞I/O、I/O多路复用、异步I/O、存储映射I/O、以及文件锁,其中有许多的功能,我们将会在后面的提高篇和进阶篇章节实例中使用到。

  

l 非阻塞I/O:进程向文件发起I/O操作,使其不会被阻塞。

  

l I/O多路复用:select()和poll()函数。

  

l 异步I/O:当文件描述符上可以执行I/O操作时,内核会向进程发送信号通知它。

  

l 存储映射I/O:mmap()函数。

  

l 文件锁:flock()、fcntl()以及lockf()函数。

相关文章