最新消息:比度技术-是关注互联网技术的个人博客,大部分内容来自互联网,以作为笔记查阅。

Linux write写文件原子性分析以及文件锁

编程开发 bidu 68浏览

Linux  write写文件原子性分析以及文件锁

文件操作:

大多数的unix和linux都将write设计为原子操作,但这只限于文件,对于管道(pipe),套接字(socket),FIFO 又应当别论了。内核在写文件之前会对该文件加锁,不管是否成功完成写操作,在返回之前都会解锁,write分为定位和写入两个阶段,定位操作指定内容写入文件的位置(如 “起始”,“末尾”,或是中间某一位置 ,还记得lseek这个函数吧,它就是完成定位操作的),如果多个进程都需要将数据添加到某一文件,那么为了保证定位和写数据这两步是一个原子操作,需要在打开文件时设置O_APPEND标志,看到这里我们就会想,虽然保证了定位和写数据是一个原子操作,但是是否能够保证多个进程或线程写入的数据不会交错呢?两种情况:磁盘已满或则要写入的文件的大小超过了当前进程的文件大小限制。其实至少还有一种情况,那就是内核中的高速缓存不够用的时候,比如linux内核在发现高速缓存不够用的时候就只写入实际能够容下的数据然后返回,也就是当然如果你将buf的内容设定的非常大,超过了内核的缓存,则可能出现非原子操作的情况,当然这种情况我们应该避免发生。

如果我们非要在同一个文件中记录多个进程产生的数据,我们最好采用unix日志系统采用的方法,用一个专用进程处理文件IO,其它进程把需要写的数据发送给这个专用进程,这样应该比多个进程同时写一个文件可靠和高效。

2.管道

SUS对管道的写操作说得更多也更明确,我们只需遵照其标准就可以了。对于write(pipefd,buf,nbyte),其要点如下:

如果nbyte<=PIPE_BUF,不管O_NONBLOCK是否设置,其写操作都是原子的,就是说多个进程都在此条件下同时写同一个管道不会引起数据交错。

如果nbyte>PIPE_BUF,是不能保证写操作是原子的,写入的数据可能与其他进程写入的数据交错。

3.socket

SUS中对于写socket并没有说很多,我们无法从标准中得知write是否保证写操作的原子性。我看了一下linux2.6.14内核关于tcp数据的写操作,发现它不是原子的,也从网上查到了这部分代码的作者(们)对这个问题的看法,他(们)认为对一个可能永久阻塞的操作保证原子性是错误的。我们也只能姑且这么认为了

参考:http://os.51cto.com/art/201108/285324.htm

 

4.并发,多个进程对同一文件写入的问题及文件共享.

一、基本概念

内核使用三种数据结构表示打开的文件,它们之间的关系决定了在文件共享方面一个进程对另一个进程可能产生的影响

1.每个进程在进程表都有一个记录项,记录项中包含有一张打开文件描述符表,与文件描述符相关联的是:

a)文件描述符标志

b)指向一个文件表项的指针,父子进程共享一个文件表项

 

2.内核为所有打开文件维持一张文件表,每个文件表项包括

a)文件状态标志(读、写、同步、非阻塞等)

b)当前文件偏移量

c)指向该文件v节点表项的指针

 

3.每个打开文件都有一个v节点结构,包含了文件的所有者,文件长度,文件所在设备等信息

当多个进程同时修改一个文件时,通过v节点结构影响其他进程的读写。

 

参考:https://blog.csdn.net/midion9/article/details/50518595

并发环境下,多个进程对同一文件写入的问题涉及文件共享.

Linux系统支持不同进程间共享打开的文件。内核使用三种数据结构表示打开的文件:进程表项、文件表项、v节点表(linux下inode节点)。内核使用三种数据结构表示打开的文件,它们之间的关系决定了在文件共享方面一个进程对另一个进程可能产生的影响。

(1) 每个进程在进程表中都有一个记录项,记录项中包含有一张打开文件描述符表,可将其视为一个矢量,每个描述符占用一项。与每个文件描述符相关联的是:

(a) 文件描述符标识(close_on_exec)。

(b)指向一个文件表项的指针。

(2)内核为所有的打开文件维持一张文件表。每个文件表项包含:

(a)文件状态标志(读、写、添加、同步和非阻塞等)。

(b)当前文件偏移量。

(c)指向该文件v节点的指针。

(3)每个打开文件(或设备)都有一个v节点(v-node)结构。v节点包含了文件类型和对此文件进行各种操作的函数的指针。对于大多数文件,v节点还包含了该文件的i节点(i-node,索引节点)。这些信息是在打开文件时从磁盘上读入内存的,所以所有关于文件的信息都是快速可供使用的。例如,i节点包含了文件的所有者,文件长度,文件所在的设备,指向文件实际数据块在磁盘上所在位置的指针等等。

注意:Linux没有使用v节点,而是使用了通用i节点结构。虽然两种实现有所不同,但在概念上,v节点与i节点是一样的。两者都指向文件系统特有的i节点结构。

如果两个独立进程各自打开了同一个文件,则有图2中所示的安排。我们假设第一个进程在文件描述符3上打开该文件,而另一个进程则在文件描述4上打开该文件。打开该文件的每一个进程都得到一个文件表项,但对一个给定的文件只有一个v节点表项。每个进程都有自己的文件表项的一个理由是:这种安排使每一个进程都有它自己的对该文件的当前偏移量。下图:两个进程各自独立打开同一个文件

给出了这种数据结构后,现在对前面所描述的操作做进一步说明。

在完成每个write后,在文件表项中的当前文件偏移量即增加所写的字节数。如果当前文件偏移量超过了当前文件长度,则在i节点表项中的当前文件长度被设置为当前文件的偏移量。

如果用O_APPEND标志打开了一个文件,则相应标志也被设置到文件表项的文件状态标志中。每次对这种具有添写标志的文件执行写操作时,在文件表项中的当前文件偏移量首先被设置为i节点表项中的文件长度。这就使得每次写的数据都添加到文件的当前尾端处。

在对open函数的O_CREAT和O_EXCL选项进行说明时,我们已经见到另一个有关原子操作的例子。当同时指定这两个选项,而该文件又已经存在时,open将失败。

5.注意三个概念:文件描述符标志、文件状态标志、文件描述符。

文件描述符标志(close_on_exec),它仅仅是一个标志,当fork了一个子进程,然后在子进程中调用了exec函数时就用到了该标志,它的含义就是,执行exec钱是否要关闭这个文件描述符,它的作用域只用于一个进程的一个描述符。而文件状态标志是在系统文件表中,读写可执行等这些标志,适用于指向该给定文件表项的任何进程中的所有描述符。

(二)既然文件可以共享,那不同进程间共享同一个文件时,就可能出现并发竞态等问题。针对不同的竞态问题,我们有不同的策略。

1、若两个进程同时对一个文件进行写操作,则可能使其中一个的写数据被另外一个的写数据覆盖,针对这种情况,采取的办法是在open打开文件时设置O_APPEND标志。

2、Linux还允许原子性地定位搜索(seek)和执行I/O。其中pread和pwrite就是这种的函数。

#include <unistd.h>

ssize_t pread(int fd, void *buf, size_t count, off_t offset);

ssize_t pwrite(int fd, const void *buf, size_t count, off_t offset);

调用pread相当于顺序调用lseek和read,但pread又与这种顺序调用有下列区别:

a、调用pread时,无法中断其定位和读操作。

b、不更改文件指针

调用pwrite相当于顺序调用lseek和write,但也与他们有类似的区别。

(三)可以使用dup和dup2函数复制一个现存的文件描述符

#include <unistd.h>

int dup(int oldfd);   // 返回的新文件描述符一定是当前可用文件描述符中的最小数值。

int dup2(int oldfd, int newfd);  // 可以用newfd参数指定新描述符的数值,如果newfd已经打开,则现先将其关闭。如果newfd等于oldfd,则dup2返回newfd,而不关闭它。

 

文件删除

一般删除都是文件索引,如果两个进程或线程同时打开同一个文件,一个线程执行删除操作,只要另一个线程不退出,就可以继续对该文件进行操作,一旦退出才找不到该文件的索引节点而报错

 

 

6.文件描述符复制

dup和dup2函数,

下面两个函数都可用来复制一个现存的文件描述符:

#include <unistd.h>

int dup(int filedes);

int dup2(int filedes, int filedes2);

两函数的返回值:若成功则返回新的文件描述符,若出错则返回-1

可以使用dup2进行 rotate日志文件:

rename函数调用,重命名文件名后 原先打开的文件句柄fd 还是指向老的iNode 也就是重命名后的文件。

newfd = open(filename, O_WRONLY | O_APPEND | O_CREAT, 0644);

oldfd = dup2(newfd,oldfd); 这里原先打开的文件句柄fd 指向了新inode也就是新的空文件。

close(newfd)

 

怎样判断文件fd是不是指向原来的文件:

可以根据:struct stat 结构的st_ino 是否一致判断

struct stat {

dev_t         st_dev;       //文件的设备编号

ino_t         st_ino;       //节点

mode_t        st_mode;      //文件的类型和存取的权限

nlink_t       st_nlink;     //连到该文件的硬连接数目,刚建立的文件值为1

uid_t         st_uid;       //用户ID

gid_t         st_gid;       //组ID

dev_t         st_rdev;      //(设备类型)若此文件为设备文件,则为其设备编号

off_t         st_size;      //文件字节数(文件大小)

unsigned long st_blksize;   //块大小(文件系统的I/O 缓冲区大小)

unsigned long st_blocks;    //块数

time_t        st_atime;     //最后一次访问时间

time_t        st_mtime;     //最后一次修改时间

time_t        st_ctime;     //最后一次改变时间(指属性)

};

参考:https://www.cnblogs.com/sylar5/p/6491033.html

st_mode是用特征位来表示文件类型的,特征位的定义如下:

S_IFMT      0170000     文件类型的位遮罩

S_IFSOCK    0140000     socket

S_IFLNK     0120000     符号链接(symbolic link)

S_IFREG     0100000     一般文件

S_IFBLK     0060000     区块装置(block device)

S_IFDIR     0040000     目录

S_IFCHR     0020000     字符装置(character device)

S_IFIFO     0010000     先进先出(fifo)

S_ISUID     0004000     文件的(set user-id on execution)位

S_ISGID     0002000     文件的(set group-id on execution)位

S_ISVTX     0001000     文件的sticky位

S_IRWXU     00700       文件所有者的遮罩值(即所有权限值)

S_IRUSR     00400       文件所有者具可读取权限

S_IWUSR     00200       文件所有者具可写入权限

S_IXUSR     00100       文件所有者具可执行权限

S_IRWXG     00070       用户组的遮罩值(即所有权限值)

S_IRGRP     00040       用户组具可读取权限

S_IWGRP     00020       用户组具可写入权限

S_IXGRP     00010       用户组具可执行权限

S_IRWXO     00007       其他用户的遮罩值(即所有权限值)

S_IROTH     00004       其他用户具可读取权限

S_IWOTH     00002       其他用户具可写入权限

S_IXOTH     00001       其他用户具可执行权限

摘自《Linux C 函数库参考手册》

判断文件类型时,用对文件的st_mode的值与上面给出的值相与,再比较。比如:

#include <sys/stat.h>

#include <unistd.h>

#include <stdio.h>

 

int main()

{

int ft;

struct stat buf;

stat(“/home”, &buf);

ft= buf.st_mode & S_IFDIR;//与对应的标志位相与

if(ft== S_IFDIR)          //结果与标志位比较

printf(“It’s a directory.\n”);

return 0;

}

还有一个简单的方法,文件类型在POSIX中定义了检查这些类型的宏定义:

S_ISLINGK(st_mode)      判断是否位符号链接

S_ISREG(st_mode)        是否为一般文件

S_ISDIR(st_mode)        是否为目录

S_ISCHR(st_mode)        是否位字符装置文件

S_ISBLK(s3e)            是否先进先出

S_ISSOCK(st_mode)       是否为socket

可以根据这些宏函数的返回值判断

7.linux中inode号:

i_ino的分配是在fs/inode.c中的new_inode定义的,该函数为指定的super_block分配一个inode,当然包括为该inode指定i_ino,,last_ino便是每个文件inode号的变量值,定义为一个静态变量,每次调用时便自加1,这样便使到每个inode都有不同的i_ino :)

注意:不是所有的inode号都要通过new_inode来分配的,比如的文件系统root的inode。像ext2文件系统是给root指定一个EXT2_ROOT_INO——该值等于2。

8.文件锁(flock)

文件锁的类型

1、读锁:共享锁,如果A进程对文件的某的区域加了读锁,B进程也可以在此区域加读锁,但是不能对此区域加写锁。

2、写锁:读占锁,如果A进程对文件的某个区域加了写锁,B进程就不能对此区域加写锁,也不能对此区域加读锁。当多个进程同时对一个文件进行读写操作时,为确保文件的完整和一致,这几个进程要加锁同步。当进程开始读取文件的某个区域时,先加读锁,读完之后再解锁。

使用fcntl加锁:

结构体:

struct flock{

short l_type;    //加锁的类型:F_RDLCK(读锁)   F_WRLCK(写锁)   F_UNLCK(解锁)

short l_whence; //什么文字开始加锁:SEEK_SET   SEEK_CUR    SEEK_END

off_t  l_start;      //开始加锁的位置

off_t l_len;         //加锁的长度

pid_t l_pid;        //加锁的进程ID:只在F_GETLK的时候使用

}

 

例如:

typedef struct flock fk;

int main(void)

{

int fd = open(“a.txt”,O_RDONLY|O_APPEND);

if(-1 == fd)

perror(“error”),exit(-1);

fk lock;

lock.l_type = F_RDLCK;     //加读锁

lock.l_whence = SEEK_SET;  //从文件的开始位置

lock.l_start = 10;         //加锁的开始位置为10

lock.l_len = 100;          //长度为100

 

int res = fcntl(fd,F_SETLK,&lock); //执行加锁

if(-1 == res)

perror(“error”),exit(-1);

else

puts(“SET FILE LOCK SUCESS!”);

 

lock.l_type = F_UNLCK;     //解锁

res = fcntl(fd,F_SETLK,&lock);

if(-1 == res)

perror(“error”),exit(-1);

puts(“UNLOCK FILE SUCESS!”);

}

(1)进程锁:flock

在flock 的 man page 中有关于 flock 细节的一些描述。其中说明了flock 是与打开文件的文件表项相关联的。

每个进程在进程表中都一个对应的项目,叫做进程表项,上图是最左边展示了进程表中两进程表项,分别对应两个独立的进程。在进程表项中,有一个文件描述符表,其中存储了所有本进程打开的文件描述符信息及指向对应文件表项的指针。而操作系统中,还另外有一张表,叫做文件表,其中存储的是系统中所有进程打开的文件的相关信息,其中的项目叫做文件表项(上图中间蓝色部分)。

 

在进程表项的文件描述符表中每个描述符都对应一个文件表项指针,指向文件表中的一项。v 节点表中的项目称为 v 节点表项,可以认为其中存储了真正的文件内容。

从前面图中可以看出,进程1对同一个文件打开了两次,分别对应本进程中的文件描述符 fd0 和 fd2。而下面的进程对这个文件又打开了一次,对应此进程中的 fd1描述符。要注意的是,不论是同一进程还是不同的进程,对同一文件打开时,都建立了与各fd 对应的独立的文件表项。进程使用 flock 对已打开文件加文件锁时,是加在了文件表项上

 

在flock 的man page 中关于文件表项有如下描述:

Locks created by flock() are associated with an open file table entry.

这说明进程使用 flock 对已打开文件加文件锁时,是加在了上图中间蓝色部分的文件表项上。假如图中位于下方的进程对fd1 加上了排他锁,实际就是加在了fd1 指向的文件表项上,这时上方的进程对 fd0 或 fd2 再加任何类型的文件锁都会失败。这是因为操作系统会检测到上方的两个文件表项和下方的文件表项都指向了相同的 v 节点,并且下方的文件表项已经加了排他锁,这时如果其他指向相同v 节点的文件表项再想尝试加上与原有锁类型不相容的文件锁时,操作系统会将此文件表项对应的进程阻塞。(参考:https://zhuanlan.zhihu.com/p/25134841

(2)调用dup 、 fork、execve 时的文件锁

如果要了解用dup复制文件描述符时和使用fork,产生子进程时的文件锁表现,就要了解在调用这两个函数时,描述符对应的数据结构发生了哪些变化。使用 dup 复制文件描述符, dup 复制文件描述符时,新的文件描述符和旧的文件描述符共享同一个文件表表项,示意图如下:

调用 dup 后,两个描述符指向了相同的文件表项,而flock 的文件锁是加在了文件表项上,因而如果对 fd0 加锁那么 fd1 就会自动持有同一把锁,释放锁时,可以使用这两个描述符中的任意一个。所以 对dup后的fd加同一类型锁 flock不起作用

dup2并不完全等同于close()加上fcntl.它们之间的区别是:

dup2是一个原子操作,而close及fcntl则包含两个函数调用。有可能在close和fcntl之间插入执行信号捕获函数,它可能修改文件描述符。

(3)通过 fork 产生子进程

通过fork 产生子进程时,子进程完全复制了父进程的数据段和堆栈段,父进程已经打开的文件描述符也会被复制,但是文件表项所在的文件表是由操作系统统一维护的,并不会由于子进程的产生而发生变化,因而如果父进程打开了一个文件,假设对应 fd1,那么在调用 fork 后,两个进程对应的打开文件的数据结构如下

这时父子进程中各有一个打开的fd,但是它们指向了同一个文件表项。如果在父进程中已经对 fd0 加了文件锁,由于文件锁作用于父子进程共享的文件表项上,这时相当于子进程的fd0 自动拥有了文件锁,父进程中的 fd0 和子进程中的fd0 持有的是同一个锁,因而在释放锁时,可以使用父进程或子进程中的任意一个fd。

  • 所以针对这种情况 flock(fd, LOCK_EX) 是无效的。

(4)子进程重复加锁

上面已经说明,子进程会复制父进程已经打开的文件描述符,并且子进程和父进程中的文件描述符会指向文件表中的同一个文件表项。考虑如下情形:在fork 之前,父进程已经对某一文件描述符加了文件锁,fork 产生子进程后,如果子进程对复制的文件描述符再次用 flock 加锁,并且锁的类型与父进程加的并不相同,这里会发生什么?答案就是子进程中新加的锁会生效,所有指向同一文件表项的fd 持有的锁都会变为子进程中新加的锁。可以认为,子进程新加的锁起到了修改锁类型的作用。但是锁类型相同的话,加锁不生效。

(5)execve 函数族中的文件锁

在fork 产生子进程后,一般会调用 execve 函数族在子进程中执行新的程序。如果在调用 execve 之前,子进程中某些打开的文件描述符已经持有了文件锁,那么在执行execve 时,如果没有设置 close-on-exec 标志,那么在新的程序中,原本打开的文件描述符依然会保持打开,原本持有的文件锁还会继续持有。

(5.1)关于linux进程间的close-on-exec机制

大部分这种问题都能够解决,在文章的最后,提到了一种特殊情况,就是父子进程中的端口占用情况。父进程监听一个端口后,fork出一个子进程,然后kill掉父进程,再重启父进程,这个时候提示端口占用,用netstat查看,子进程占用了父进程监听的端口。原理其实很简单,子进程在fork出来的时候,使用了写时复制(COW,Copy-On-Write)方式获得父进程的数据空间、 堆和栈副本,这其中也包括文件描述符。刚刚fork成功时,父子进程中相同的文件描述符指向系统文件表中的同一项(这也意味着他们共享同一文件偏移量)。这其中当然也包含父进程创建的socket。接着,一般我们会调用exec执行另一个程序,此时会用全新的程序替换子进程的正文,数据,堆和栈等。此时保存文件描述符的变量当然也不存在了,我们就无法关闭无用的文件描述符了。所以通常我们会fork子进程后在子进程中直接执行close关掉无用的文件描述符,然后再执行exec。但是在复杂系统中,有时我们fork子进程时已经不知道打开了多少个文件描述符(包括socket句柄等),这此时进行逐一清理确实有很大难度。我们期望的是能在fork子进程前打开某个文件句柄时就指定好:“这个句柄我在fork子进程后执行exec时就关闭”。其实时有这样的方法的:即所谓 的 close-on-exec。回到我们的应用场景中来,只要我们在创建socket的时候加上SOCK_CLOEXEC标志,就能够达到我们要求的效果,在fork子进程中执行exec的时候,会清理掉父进程创建的socket。

例如:

#ifdef WIN32

SOCKET ss = ::socket(PF_INET, SOCK_STREAM, 0);

#else

SOCKET ss = ::socket(PF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0);

#endif

 

当然,其他的文件描述符也有类似的功能,例如文件,可以在打开的时候使用O_CLOEXEC标识(linux 2.6.23才开始支持此标记),达到和上面一样的效果。或者使用系统的fcntl函数设置FD_CLOEXEC即可。

//方案A

int fd = open(“foo.txt”,O_RDONLY);

int flags = fcntl(fd, F_GETFD);

flags |= FD_CLOEXEC;

fcntl(fd, F_SETFD, flags);

//方案B,linux 2.6.23后支持

int fd = open(“foo.txt”,O_RDONLY | O_CLOEXEC);

 

现在我们终于可以完美的解决端口占用这个问题了

来源:https://blog.csdn.net/ljxfblog/article/details/41680115

(6)文件锁的解除

  • 用 LOCK_UN 解锁

文件锁的解除可以通过将 flock 的 operation 参数设置为 LOCK_UN 常量来实现。这时如果有多个fd 指向同一文件表项,例如给 fd0 加文件锁后,用 dup 复制了fd0 的情况下,用 LOCK_UN 对fd0 解锁后,所有和 fd0 指向同一文件表项的 fd 都不再持有文件锁。fork 子进程复制父进程文件描述符的情形也是如此。

  • 关闭文件时自动解解锁

对描述符fd加了文件锁后,如果没有显式使用LOCK_UN 解锁,在关闭 fd 时,会自动解除其持有的文件锁。但是在为 fd 加锁后如果调用 了dup 复制了文件描述符,这时关闭fd 时的表现和调用 LOCK_UN 是不一样的。

如果未显式使用 LOCK_UN 解锁,在关闭文件描述符后,如果还有其他的fd 指向同一文件表项,比如之前调用了dup 的情形,这时加在文件表项上的文件锁并不会解除,其他指向此文件表项的文件描述符依然持有锁,并且锁的类型也不会发生变化。

使用fork 产生子进程时同样如此。父进程和子进程的描述符指向同一文件表项且已经加了文件锁时,如果用 LOCK_UN 将其中一个fd 解锁,那么指向同一表项的所有其他fd 都会自动解锁。但是如果未使用 LOCK_UN 解锁,只是通过 close(fd) 关闭了某个文件描述符,那么指向同一文件表项的其他描述符,依然会持有原有的锁。

出于方便考虑,在没有出现多个fd 指向现一文件表项的情况下,可以直接使用close(fd) 的默认解锁功能,而不用显式的使用LOCK_UN。在有多个 fd 指向同一文件表项的情形下,如果要完全解锁,一定要使用 LOCK_UN 解锁,不能再使用 close(fd) 的默认解锁功能。

用 fork 验证以上结论的代码如下,读者可以将下面程序稍作改动,并与本文后面的python 程序配合使用,观察交替加锁的情形:

(7)注意Flock锁是操作系统级别的:

对同一文件的读写即使是完全不相关的两个进程,也可以使用flock 进行文件的读写保护。因而可以认为 flock 是操作系统级别的锁。为了验证这一观点,可以分别用不同的语言对同一文件进行了锁定和释放测试。测试表明,用 flock 对同一文件进行锁定,确实是在操作系统层面上全局生效的。下面分别是C 语言、python、php 对同一文件的锁定测试代码,可以在同一系统上同时运行其中的任意两个来验证文件锁对文件读写的全局保护特性。

(8)锁继承与释放的语义

flock()根据调用时operation参数传入LOCK_UN的值来释放一个文件锁。此外,锁会在相应的文件描述符被关闭之后自动释放。同时,当一个文件描述符被复制时(dup()、dup2()、或一个fcntl() F_DUPFD操作),新的文件描述符会引用同一个文件锁。

flock锁可递归,即通过dup或者或者fork产生的两个fd,都可以加锁而不会产生死锁

 

flock(fd, LOCK_EX);

new_fd = dup(fd);

flock(new_fd, LOCK_UN);

这段代码先在fd上设置一个互斥锁,然后通过fd创建一个指向相同文件的新文件描述符new_fd,最后通过new_fd来解锁。从而我们可以得知新的文件描述符指向了同一个锁。所以,如果通过一个特定的文件描述符获取了一个锁并且创建了该描述符的一个或多个副本,那么,如果不显示的调用一个解锁操作,只有当文件描述符副本都被关闭了之后锁才会被释放。

由上我们可以推出,如果使用fork()创建一个子进程,子进程会复制父进程中的所有描述符,从而使得它们也会指向同一个文件锁。例如下面的代码会导致一个子进程删除一个父进程的锁:

 

flock (fd, LOCK_EX);

 

if (0 == fork ()) {

flock (fd, LOCK_UN);

}

所以,有时候可以利用这些语义来将一个文件锁从父进程传输到子进程:在fork()之后,父进程关闭其文件描述符,然后锁就只在子进程的控制之下了。通过fork()创建的锁在exec()中会得以保留(除非在文件描述符上设置了close-on-exec标记并且该文件描述符是最后一个引用底层的打开文件描述的描述符)。

如果程序中使用open()来获取第二个引用同一个文件的描述符,那么,flock()会将其视为不同的文件描述符。如下代码会在第二个flock()上阻塞。

fd1 = open (“test.txt”, O_RDWD);

fd2 = open (“test.txt”, O_RDWD);

flock (fd1, LOCK_EX);

flock (fd2, LOCK_EX);

flock()的限制

flock()放置的锁有如下限制:只能对整个文件进行加锁。这种粗粒度的加锁会限制协作进程间的并发。假如存在多个进程,其中各个进程都想同时访问同一个文件的不同部分。通过flock()只能放置劝告式锁。

很多NFS实现不识别flock()放置的锁。

注释:在默认情况下,文件锁是劝告式的,这表示一个进程可以简单地忽略另一个进程在文件上放置的锁。要使得劝告式加锁模型能够正常工作,所有访问文件的进程都必须要配合,即在执行文件IO之前先放置一把锁。

 

(9)flock 命令

除了多种语言提供 flock 系统调用或函数,linux shell 中也提供了 flock 命令。

flock 命令最大的用途就是实现对 crontab 任务的串行化。在 crontab 任务中,有可能出现某个任务的执行时间超过了 crontab 中为此任务设定的执行周期,这就导致了当前的任务实例还未执行完成,crontab 又启动了同一任务的另外一个实例,这通常不是用户所期望的行为。极端情况下,如果某个任务执行异常一直未返回,crontab 不会处理这种情形,会继续启动新的实例,而新的实例很可能又会异常,这样就导致 crontab 对同一任务不断的启动新的实例,最终导致系统内存被耗尽,影响到整个操作系统的运行。为了防止crontab 任务出现多实例的情况,可以使用 flock 命令将crontab 中任务的周期性执行串行化。

 

在将corntab 中任务串行化时,flock 通过对一个中间文件加文件锁来间接实现同一时刻某个任务只有一个实例运行的目标。对应的 crontab 中任务的描述形式如下:

 

* * * * * flock -xn /tmp/mytest.lock -c ‘php /home/fdipzone/php/test.php’

这里的定时任务是每分钟执行一次,但是任务中并未直接执行目标命令 ‘php /home/fdipzone/php/test.php’ ,而是将命令作为 flock 的 -c 选项的参数。flock 命令中,-x 表示对文件加上排他锁,-n 表示文件使用非阻塞模式,-c 选项指明加锁成功后要执行的命令。因而上面flock 命令的整体含义就是:如果对 /tmp/mytest.lock 文件(如果文件不存在, flock 命令会自动创建)加锁成功就执行后面的命令,否则不执行。

假如上面 php 命令要执行2分钟,而crontab 任务每分钟就会执行一次,如果当前 php 命令正在执行,说明 flock 已经锁定了文件 /tmp/mytest.lock,crontab 到了再次执行任务的时间时,会发现文件已经被加了锁。由于设置的是非阻塞模式的文件锁,flock 会在加锁失败时直接返回,并不执行php 命令,这样就使 php 命令得以顺序执行,crontab 任务就不会出现同时有两个实例运行的情况了,达到了串行化目的。

(10)总结

Linux提供了flock(对整个文件加锁)、fcntl(对整个文件区域加锁)两个函数来做进程间的文件同步。同时也可以使用信号量来完成所需的同步,但通常使用文件锁会更好一些,因为内核能够自动将锁与文件关联起来。

flock 在多进程共享文件时非常有用,避免了用各种技巧模拟多线程中的读写锁,这样模拟出来的锁通常会有缺陷。在多进程共享文件时,强烈建议使用flock 文件件锁。一言以蔽之:简单、易用、高效。

  • 划重点:(同一进程内 同一个fd可递归不阻塞)

gfd = open(“/tmp/nx_log1.lock”, O_RDWR);

  • 对于父进程open的gfd 使用flock(fd, LOCK_EX)加锁无效,

需要在子进程分别gfd = open(“/tmp/nx_log1.lock”, O_RDWR);分别open同一文件。

flock(fd, LOCK_EX)才有效

  • 对于从父进程继承来的(open的)fd 使用flock(fd, LOCK_EX) 是无效的 会忽略 子进程的flock(fd, LOCK_EX)

/*进程内 多线程 单fd 对 append打开的文件 进行write (不调用lseek)不用加锁,没有风险 因为多线程共享进程的同一个文件表 */

同一个进程内对同一个fd,flock(fd, LOCK_EX)加锁无效,但是同一文件的不同fd 有效:

int tfd = -1,tfd1 = -1;

int ret1 =-1;

tfd =open(G_NX_LOG_PROCESS_LOCK_FILE_NAME, O_CREAT|O_RDWR);

ret1 = flock(tfd, LOCK_EX);

printf(“flock fd:%d\n”,tfd,ret1);

ret1 = flock(tfd,LOCK_EX);// not lock

printf(“flock fd:%d\n”,tfd,ret1);

 

tfd1 =open(G_NX_LOG_PROCESS_LOCK_FILE_NAME, O_CREAT|O_RDWR);

ret1 = flock(tfd1, LOCK_EX);//locked

printf(“flock fd:%d\n”,tfd,ret1);

ret1 = flock(tfd1, LOCK_EX);

printf(“flock fd:%d\n”,tfd,ret1);

 

(2)

 

伪代码:

function1(){主进程open 1次

fd =open(filename)

….

….

}

 

//主进程或子进程

function2(){

flock(fd, LOCK_EX)

write(fd, buf, len);

flock(fd, LOCK_UN)

 

if(filesize > 1024M)){

//在某个时候文件太大rotate

flock(fd, LOCK_EX)

rename(filename,newfilename);

newfd = open(temfilename);

fd = dup2(newfd,fd);

close(newfd);

flock(fd, LOCK_UN)

}

}

9. Linux下C的线程同步机制

互斥锁

通过锁的机制实现线程间的互斥,同一时刻只有一个线程可以锁定它,当一个锁被某个线程锁定的时候,如果有另外一个线程尝试锁定这个临界区(互斥体),则第二个线程会被阻塞,或者说被置于等待状态。只有当第一个线程释放了对临界区的锁定,第二个线程才能从阻塞状态恢复运行。

 

int pthread_mutex_init(pthread_mutex_t* mutex, const thread_mutexattr_t* mutexattr);初始化一个互斥锁。

 

int pthread_mutex_lock(pthread_mutex_t* mutex);如果mutex被锁定,当前进程处于等待状态;否则,本进程获得互斥锁并进入临界区。

 

int pthread_mutex_trylock(pthread_mutex_t* mutex);和lock不同的时候,尝试获得互斥锁不成功不会使得进程进入阻塞状态,而是继续返回线程执行。该函数可以有效避免循环等待锁,如果trylock失败可以释放已经占有的资源,这样可以避免死锁。

 

int pthread_mutex_unlock(pthread_mutex_t* mutex);释放互斥锁,并使得被阻塞的线程获得互斥锁并执行。

 

int pthread_mutex_destroy(pthread_mutex_t* mutex);用来撤销互斥锁的资源。

pthread_mutex_t mutex;

pthread_mutex_init(&mutex,NULL);

 

 

void pthread1(void* arg){

pthread_mutex_lock(&mutex);

…..//临界区

pthread_mutex_unlock(&mutex);

}

 

 

void pthread2(void* arg){

pthread_mutex_lock(&mutex);

…..//临界区

pthread_mutex_unlock(&mutex);

}

读写锁

读写锁与互斥量类似,不过读写锁允许更高的并行性。适用于读的次数大于写的次数的数据结构。一次只有一个线程可以占有写模式的读写锁,但是多个线程可以同时占有读模式的读写锁。读锁锁住,加读锁,可以;加写锁会被阻塞,但此时会阻塞后续的读锁请求,防止读锁长期占用无法进入写模式。写锁就是互斥锁。

int pthread_rwlock_init(pthread_rwlock_t* rwlock, const pthread_rwlockattr_t* attr);初始化读写锁

int pthread_destroy(pthread_rwlock_t* rwlock);销毁读写锁

int pthread_rwlock_rdlock(pthread_rwlock_t* rwlock);加读锁

int pthread_rwlock_wrlock(pthread_rwlock_t* rwlock);加写锁

int pthread_rwlock_unlock(pthread_rwlock_t* rwlock);解锁

条件变量

信号量只有锁住和不锁两种状态,而且当条件变量和信号量一起使用时,允许线程以无竞争的方式等待特定的条件发生。

条件本身是由互斥量保护的:线程在改变条件状态之前必须先锁住互斥量。

int pthread_cond_init(pthread_cond_t* cond,const pthread_condattr_t* attr);初始化动态分配的条件变量;也可以直接用PTHREAD_INITIALIZER直接赋值给静态的条件变量

int pthread_cond_destroy(pthread_cond_t* cond)撤销条件变量资源;

int pthread_cond_wait(pthread_cond_t* cond, pthread_mutex_t* mutex);使用该函数使得等待条件变量为真。线程被条件变量cond阻塞。

int pthread_cond_timedwait(pthread_cond_t* cond, pthread_mutex_t* mutex,const struct timespec* tspr);与wait类似,只是经历tspr时间后,即使条件变量不满足,阻塞也被解除,返回错误码。

int pthread_cond_signal(pthread_cond_t* cond);唤醒因为条件变量阻塞的线程。

int pthread_cond_broadcast(pthread_cond_t* cond);唤醒等待该条件的所有线程。

自旋锁

互斥量阻塞线程的方式是使其进入睡眠,而自旋锁是让线程忙等,即不会使其睡眠,而是不断循判断自旋锁已经被解锁。

适用于占用自旋锁时间比较短的情况。

信号量

介绍一下POSIX(POSIX标准定义了操作系统应该为应用程序提供的接口标准,换句话说,为一个POSIX兼容的操作系统编写的程序,应该可以在任何其它的POSIX操作系统(即使是来自另一个厂商)上编译执行。)的信号量机制,定义在头文件/usr/include/semaphore.h

1)初始化一个信号量:sem_init()

int sem_init(sem_t* sem,int pshared,unsigned int value);

pshared为0时表示该信号量只能在当前进程的线程间共享,否则可以进程间共享,value给出了信号量的初始值。

2)阻塞线程

sem_wait(sem_t* sem)直到信号量sem的值大于0,解除阻塞后将sem的值减一,表明公共资源经使用后减少;sem_trywait(sem_t* sem)是wait的非阻塞版本,它直接将sem的值减一,相当于P操作。

3)增加信号量的值,唤醒线程

sem_post(sem_t* sem)会使已经被阻塞的线程其中的一个线程不再阻塞,选择机制同样是由线程的调度策略决定的。相当于V操作。

3)释放信号量资源

sem_destroy(sem_t* sem)用来释放信号量sem所占有的资源

 

10.什么是自旋锁

基本概念:何谓自旋锁?它是为实现保护共享资源而提出一种锁机制。其实,自旋锁与互斥锁比较类似,它们都是为了解决对某项资源的互斥使用。无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者,也就说,在任何时刻最多只能有一个执行单元获得锁。但是两者在调度机制上略有不同。对于互斥锁,如果资源已经被占用,资源申请者只能进入睡眠状态。但是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,”自旋”一词就是因此而得名

 

(1)自旋锁(Spinlock)是一种广泛运用的底层同步机制。自旋锁是一个互斥设备,它只有两个值:“锁定”和“解锁”。它通常实现为某个整数值中的某个位。希望获得某个特定锁得代码测试相关的位。如果锁可用,则“锁定”被设置,而代码继续进入临界区;相反,如果锁被其他人获得,则代码进入忙循环(而不是休眠,这也是自旋锁和一般锁的区别)并重复检查这个锁,直到该锁可用为止,这就是自旋的过程。“测试并设置位”的操作必须是原子的,这样,即使多个线程在给定时间自旋,也只有一个线程可获得该锁。

自旋锁对于SMP和单处理器可抢占内核都适用。可以想象,当一个处理器处于自旋状态时,它做不了任何有用的工作,因此自旋锁对于单处理器不可抢占内核没有意义,实际上,非抢占式的单处理器系统上自旋锁被实现为空操作,不做任何事情。

曾经有个经典的例子来比喻自旋锁:A,B两个人合租一套房子,共用一个厕所,那么这个厕所就是共享资源,且在任一时刻最多只能有一个人在使用。当厕所闲置时,谁来了都可以使用,当A使用时,就会关上厕所门,而B也要使用,但是急啊,就得在门外焦急地等待,急得团团转,是为“自旋”,这也是要求锁的持有时间尽量短的原因!

  • 自旋锁有以下特点:

用于临界区互斥

在任何时刻最多只能有一个执行单元获得锁

要求持有锁的处理器所占用的时间尽可能短

等待锁的线程进入忙循环

  • 补充:

临界区和互斥:对于某些全局资源,多个并发执行的线程在访问这些资源时,操作系统可能会交错执行多个并发线程的访问指令,一个错误的指令顺序可能会导致最终的结果错误。多个线程对共享的资源的访问指令构成了一个临界区(critical section),这个临界区不应该和其他线程的交替执行,确保每个线程执行临界区时能对临界区里的共享资源互斥的访问。

  • 自旋锁较互斥锁之类同步机制的优势

 

2.1 休眠与忙循环

互斥锁得不到锁时,线程会进入休眠,这类同步机制都有一个共性就是 一旦资源被占用都会产生任务切换,任务切换涉及很多东西的(保存原来的上下文,按调度算法选择新的任务,恢复新任务的上下文,还有就是要修改cr3寄存器会导致cache失效)这些都是需要大量时间的,因此用互斥之类来同步一旦涉及到阻塞代价是十分昂贵的。

一个互斥锁来控制2行代码的原子操作,这个时候一个CPU正在执行这个代码,另一个CPU也要进入, 另一个CPU就会产生任务切换。为了短短的两行代码 就进行任务切换执行大量的代码,对系统性能不利,另一个CPU还不如直接有条件的死循环,等待那个CPU把那两行代码执行完。

 

2.2 自旋过程

当锁被其他线程占有时,获取锁的线程便会进入自旋,不断检测自旋锁的状态。一旦自旋锁被释放,线程便结束自旋,得到自旋锁的线程便可以执行临界区的代码。对于临界区的代码必须短小,否则其他线程会一直受到阻塞,这也是要求锁的持有时间尽量短的原因!

Api:

#include <pthread.h>

int pthread_spin_destroy(pthread_spinlock_t *lock);

int pthread_spin_init(pthread_spinlock_t *lock, int pshared);

 

int pthread_spin_lock(pthread_spinlock_t *lock);

int pthread_spin_trylock(pthread_spinlock_t *lock);

int pthread_spin_unlock(pthread_spinlock_t *lock);

自旋锁的特点与适用场景

Linux自旋锁spinlock同一时刻只能被一个可执行线程持有。当一个线程试图获取一个已经被持有的spin lock时,就会一直忙循环-选择-等待锁重新可用。忙等待免去了线程挂起再被唤醒的转换,省去了两次上下文切换的时间。因而spinlock适合下面的场景:SMP多核系统中,持有自旋锁的时间小于完成两次上下文切换的时间,这种场景使用spinlock效率会比较高。所以一般在用户态程序中,很少会遇到使用自旋锁的场景,因为用户程序由内核调度,除非绑核一般无法保证两个需要同步的线程恰好分布在两个核上,也很少真正有持有锁时间很短少于两次上下文切换时间的。

Linux spinlock在内核中相对比较常见,因为内核中有很多需要短期加锁的场景,比如在SMP系统的中断上下文中。但是,单核CPU或者禁止内核抢占时,编译的时候自旋锁会被完全剔除出内核。这点也不难理解,单核本质上串行执行任务的,靠时间片分时执行各任务来实现并发,所以单核使用spinlock会忙等待到线程的时间片用完,这是得不偿失的。Linux spinlock不可递归,可用在中断处理程序中,(中断处理程序中不能用信号量,因为会导致睡眠)。一个注意点事:中断处理程序中处理自旋锁时,一定要在获取锁之前,关闭当前核的中断,防止在中断中又去试图获得锁而造成死锁。

在           用户态程序,极少有场景会适合用自旋锁。目前在实际工程中只见过涉及绑核的高性能转发有使用过,其他业务场景的同步基本靠互斥量和条件变量。

 

文件系统、C 标准库IO缓冲区和内核缓冲区

在文件系统中,有三大缓冲为了提升效率:inode缓冲区、dentry缓冲区、块缓冲(参考:https://blog.csdn.net/shanshanpt/article/details/39258373

 

C标准库的I/O缓冲区有三种类型:全缓冲、行缓冲和无缓冲。当用户程序调用库函数做写操作时, 不同类型的缓冲区具有不同特性。

全缓冲 :如果缓冲区写满了就写回内核。常规文件通常是全缓冲的。

行缓冲 :如果用户程序写的数据中有换行符就把这一行写回内核,或者如果缓冲区写满了就写回内 核。标准输入和标准输出对应终端设备时通常是行缓冲的。

无缓冲 :用户程序每次调库函数做写操作都要通过系统调用写回内核。标准错误输出通常是无缓冲的,这样用户程序产生的错误信息可以尽快输出到设备。

除了写满缓冲区、写入换行符之外,行缓冲还有两种情况会自动做Flush操作。如果:

用户程序调用库函数从无缓冲的文件中读取

或者从行缓冲的文件中读取,并且这次读操作会引发系统调用从内核读取数据

C标准库和内核之间的关系就像在“Memory Hierarchy”中 CPU、Cache和内存之间的关系一样,C标准库之所以会从内核预读一些数据放 在I/O缓冲区中,是希望用户程序随后要用到这些数据,C标准库的I/O缓冲区也在用户空间,直接 从用户空间读取数据比进内核读数据要快得多。另一方面,用户程序调用fputc 通常只是写到I/O缓 冲区中,这样fputc 函数可以很快地返回,如果I/O缓冲区写满了,fputc 就通过系统调用把I/O缓冲 区中的数据传给内核,内核最终把数据写回磁盘或设备。有时候用户程序希望把I/O缓冲区中的数据立刻 传给内核,让内核写回设备或磁盘,这称为Flush操作,对应的库函数是fflush,fclose函数在关闭文件 之前也会做Flush操作

内核空间是进程共享的, 而c标准库的I/O缓冲区则不具有这一特性,因为进程的用户空间是完全独立的.

第13章 文件I/O缓冲(来源:https://blog.csdn.net/xiaofei0859/article/details/51147365

出于速度和效率考虑,系统I/O调用(即内核)和标准C语言库I/O函数(即stdio函数)在操作磁盘文件时会对数据进行缓冲。本章描述了这两种类型的缓冲,并讨论了其对应用程序性能的影响。本章还讨论了可以屏蔽或影响缓冲的各种技术,以及直接I/O技术–在某些需要绕过内核缓冲的场景中非常有用。

13.1  文件I/O的内核缓冲:缓冲区高速缓存(1)

read()和write()系统调用在操作磁盘文件时不会直接发起磁盘访问,而是仅仅在用户空间缓冲区与内核缓冲区高速缓存(kernel buffer cache)之间复制数据。例如,如下调用将3个字节的数据从用户空间内存传递到内核空间的缓冲区中:

write()随即返回。在后续某个时刻,内核会将其缓冲区中的数据写入(刷新至)磁盘。(因此,可以说系统调用与磁盘操作并不同步。)如果在此期间,另一进程试图读取该文件的这几个字节,那么内核将自动从缓冲区高速缓存中提供这些数据,而不是从文件中(读取过期的内容)。与此同理,对输入而言,内核从磁盘中读取数据并存储到内核缓冲区中。read()调用将从该缓冲区中读取数据,直至把缓冲区中的数据取完,这时,内核会将文件的下一段内容读入缓冲区高速缓存。(这里的描述有所简化。对于序列化的文件访问,内核通常会尝试执行预读,以确保在需要之前就将文件的下一数据块读入缓冲区高速缓存中。更多关于预读的内容请参考13.5节。)

采用这一设计,意在使read()和write()调用的操作更为快速,因为它们不需要等待(缓慢的)磁盘操作。同时,这一设计也极为高效,因为这减少了内核必须执行的磁盘传输次数。

Linux内核对缓冲区高速缓存的大小没有固定上限。内核会分配尽可能多的缓冲区高速缓存页,而仅受限于两个因素:可用的物理内存总量,以及出于其他目的对物理内存的需求(例如,需要将正在运行进程的文本和数据页保留在物理内存中)。若可用内存不足,则内核会将一些修改过的缓冲区高速缓存页内容刷新到磁盘,并释放其供系统重用。

更确切地说,从内核2.4开始,Linux 不再维护一个单独的缓冲区高速缓存。相反,会将文件I/O缓冲区置于页面高速缓存中,其中还含有诸如内存映射文件的页面。然而,正文的讨论采用了”缓冲区高速缓存(buffer cache)”这一术语,因为这是UNIX 实现中历史悠久的通称。

总之,如果与文件发生大量的数据传输,通过采用大块空间缓冲数据,以及执行更少的系统调用,可以极大地提高I / O 性能。

转载请注明:比度技术-关注互联网技术的个人博客 » Linux write写文件原子性分析以及文件锁