虚位以待(AD)
虚位以待(AD)
首页 > 操作系统 > Unix/BSD > Unix文件的共享

Unix文件的共享
类别:Unix/BSD   作者:码皇   来源:互联网   点击:

1 内核使用三种数据结构表示打开的文件,它们之间的关系决定了在文件共享方面一个进程对另一个进程的影响。(1) 每个进程在进程表中都一个记录项(task_struct),包含一个打开文件描述符表(存放在用户空间)。每

 

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

 

(1). 每个进程在进程表中都一个记录项(task_struct),包含一个打开文件描述符表(存放在用户空间)。每个文件描述符为表中一项,包括文件描述符标识和指向一个文件表项的指针。

 

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

a). 文件状态标识(RD, WR, APPEND, 同步和非阻塞等)。注意,文件描述符作用域是一个进程,而文件状态标识则适用于指向文件表中该表项的所有进程的描述符。使用fcntl函数来修改这两个结构。下面第3节将会解释。

b). 当前文件偏移量

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

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

Linux没有使用v-node,而是使用了通用的i-node节点结构。虽然两者实现有所不同,但在概念上,v-node和i-node是一样的,都指向文件系统相关的i-node结构。

三张表的关系,进程表项有一个进程打开了标准输出(fd=0)和标准输入(fd=1)两个文件。所以task_struct结构的打开文件描述符表中有这两项(每项包含fd标志和文件指针),其中文件指针指向文件表(每项包含文件状态标志,当前文件偏移量和v-node指针),其中v-node指针指向v-node节点表(每项包含v-node info, i-node info&当前文件长度等)。Linux没有将相关数据结构分为i-node和v-node,而是使用一个独立于文件系统的i-node和一个依赖文件系统的i-node。

如果两个进程打开了同一个文件,则该文件在文件表中有两项,这样可以记录每个进程对该文件的当前偏移量。但是v-node项中只有一项。在完成每个write操作后,在文件表项中当前文件偏移量即增加所写的字节数。如果文件偏移量超过了文件长度,则文件长度更新为当前偏移量。所以在使用O_APPEND标识打开一个文件,则相应标识也被设置到文件表项中。每次写文件操作,首先会设置i-node表项的文件长度,再将写的数据添加到文件末尾。无论lseek定位到哪里,都只会修改当前文件表项的偏移量,而没有进程任何I/0操作。

fork—父子进程会对每一个打开的文件描述符共享一个文件表表项。

dup会使多个文件描述符指向同一表项。

 

2.  由于文件表中记录了每个进程的当前偏移量,所以多个进程同时读一个文件可以正确工作。但是多个进程写同一个文件就会产生的预料不到的结果,需要原子操作。

例如多个进程使用O_APPEND选项来open一个文件,实际上这个写的操作在原来版本的实现中相当于如下形式 :

if (lseek(fd, 0L, 2) < 0)

err_sys(“can not seek”);

if(lseek(fd, buf, 100) != 100)

err_sys(“can not write”)

如果不是原子操作,多个进程同时使用这种方法添加数据就会产生问题。(比如应用服务器中多线程执行的多个组件写日志操作。)

在UNIX中扩展了lseek+read/write的原子性操作函数,原型为

#include <unistd.h>

ssize_t pread(int filedes, void *buf, size_t nbytes, off_t offset);

ssize_t pwrite(int filedes, void *buf, size_t nbytes, off_t offset);

从filedes的offset处(从文件开始位置的偏移量)读取/写入nbytes个字符。文件当前偏移量不改变。

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

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

2. 不更新文件指针。

调用pwrite相当于顺序调用lseek和write,同样是原子操作且无法中断。

3. 另一种需要原子操作的情形:在使open函数

自动创建文件时,即不带选项O_CREAT,O_EXCL(如果文件已经存在,则出错。如果不存在则创建该文件),以避免抹掉原有文件的记录。

if ((fd=open(pathname, O_WRONLY)) < 0) {

if(errno == ENOENT) {    //no such file or directory

if ((fd = creat(pathname, mode)) < 0)

err_sys(“creat error”);

} else {

err_sys(“open error”);

}

}

同样在open和creat之间也可能导致问题。例如进程A执行open没有检测到原有文件,然后调度到进程B创建文件并写入了一些东西。这是进程A继续执行creat部分,新创建的文件会清除进程B创建的文件。需要引入记录锁机制(APUE Chapter 14.3)。

记录锁record locking的功能是:当一个进程读或者修改文件的某个部分时,阻止其他进程修改同一文件。由于UNIX系统内核没有使用文件记录的概念,所以更适合的而属于应该是字节范围锁,因为它锁定的只是文件的一个区域。

Linux2.4 以上的内核支持flock函数(锁住整个文件), fcntl记录锁和lockf。

#include <fcntl.h>

int fcntl(int filedes, int cmd, …/*struct flock */);

struct flock{

short l_type;    /*F_RDLCK, F_WRLCK or F_UNLCK*/

/*共享读锁,独占性写锁和解锁一个区域*/

off_t l_start;   /*offset in bytes, relative to l_whence*/

/*要加锁的区域起始字节偏移量*/

short l_whence;/*SEEK_SET, SEEK_CUR, SEEK_END*/

off_t l_len;     /*length, in bytes; 0 means lock to EOF*/

/*所以为了锁住整个文件需要将whence,start设为SEEK_SET 0, l_len为0*/

pid_t l_pid;     /*returned with F_GETLK*/

};

锁的类型分为两种F_RDLCK共享读锁和独占写锁F_WRLCK。基本规则是:多个进程在一个给定的字节上可以有一把共享的读锁,但是在一个给定字节上只能有一个进程独用一把写锁。即如果在某个字节上已经有一把独占性写锁,则不能再对它加任何读锁。

不同进程之间锁请求:

                 读锁    写锁

无锁             允许    允许

一个或多个读锁   允许    拒绝

一个写锁     拒绝    拒绝

相同进程之间锁请求:

如果一个进程对一个文件区间已经有一把锁,该进程后面又企图在同一文件区间再加一个锁,那么新锁将替换老锁。

加读锁时,该描述副必须是读打开的。加写锁锁必须好是写打开的。

F_GETLK判断由flockptr所描述的锁是否会被另一把锁所排斥。如果存在一把锁,它阻止创建由flockptr锁描述锁。如果不存在,则除了将l_type设置为F_UNLCK;

F_SETLK

F_SETLKW F_SETLK的阻塞版本。如果请求创建的锁已经可用,可进程被唤醒。

锁的隐含集成和释放:

(1).进程中止时,所有建立的锁都释放。

(2).关闭一个描述符,则该进程通过这一描述符可以引用的任何一把锁(前提是这些锁由该进程设置的)都将释放。例如

fd1=open(pathname, …);

read_lock(fd1, …);

fd2 = dup(fd1);

close(fd2);

由于释放了fd2,这时fd1设置的锁也会被释放。

fork产生的子进程不集成父进程锁设置的锁。子进程需要调用fcntl才能获得自己的锁。这样防止父进程和子进程同时写一个文件。

在执行exec后,新程序可以继承原执行程序的锁。但是注意,如果对一个文件描述符设置了close-on-exec那么exec时关闭文件描述符,相应的文件所有锁都被释放了。

建议锁又称协同锁。对于这种类型的锁,内核只是提供加减锁以及检测是否加锁的操作,但是不提供锁的控制与协调工作。也就是说,如果应用程序对某个文件进行操作时,没有检测是否加锁或者无视加锁而直接向文件写入数据,内核是不会加以阻拦控制的。因此,建议锁,不能阻止进程对文件的操作,而只能依赖于大家自觉的去检测是否加锁然后约束自己的行为;

强制锁,是OS内核的文件锁。每个对文件操作时,例如执行open、read、write等操作时,OS内部检测该文件是否被加了强制锁,如果加锁导致这些文件操作失败。也就是内核强制应用程序来遵守游戏规则;

例如vim使用建议锁来编辑一个文件,打开多个文件时会提示这个文件已加锁/处于编辑中。但是对于没有使用建立锁的编辑器依然可以打开并编辑这个文件,文件的最后状态取决于最后一个操作文件的进程。

4. dup和dup2函数:用于复制一个现存的文件描述符。

#include <unistd.h>

int dup(int filedes);   //返回分配的是当前可用文件描述符的最小值

//它的操作等效于fcntl(filedes, F_DUPFD, 0);

int dup2(int filedes, int filedes2);    //复制结果为返回filedes2指定的描述符。如果filedes2指定的描述符已经打开,则先关闭它。如果filedes和filedes2相同则不关闭。它的操作等效于close(filedes2);fcntl(filedes, F_DUPFD, filedes2);但是dup2是一个原子操作,不用担心同步问题。

这两个函数返回的文件描述符与参数filedes共享一个文件表项。

 

5. sync, fsync和fdatasync函数

    当将数据写入文件时,内核通常先将该数据复制到其中一个缓冲区中,如果该缓冲区尚未写满。则并不将其排入输出队列。而是等待其写满或者当内核需要重用该缓冲区以便存放其他磁盘块数据时,再将该缓冲区排入输出队列。然后待其到达队首时,才进行实际的I/O才做,这种输出方式成为延时写delayed write。可以减少磁盘I/O次数,缺点是降低了文件系统的更新次数。系统故障时可能造成内容丢失。UNIX系统提供了sync、fsync和fdatasync三个函数来保证磁盘上实际文件系统与缓冲区高速缓冲中内容的一致性。

#include <unistd.h>

int fsync(int filedes);

int fdatasync(int filedes);

void sync(void);

sync只将所有修改过的块缓冲区排入写队列,然后就返回。不等待实际的磁盘操作结束。通常成为update的系统守护进程会周期性地(30s)调用sync函数。这就保证定期冲洗内核的块缓冲区。

fsync只对文件描述符filedes指定的单一文件起作用,并等待写磁盘操作然后返回。fsync对数据库这样的应用程序确保了修改过的块会写回到磁盘上。(比如Oracle的redo log需要这种操作!)

fdatesync 函数类似fsync,但它只影响文件的数据部分。而除数据之外,fsync还包括文件属性的更新。

6. fcntl函数:改变已打开文件的性质。

#include <fcntl.h>

int fcntl(int filedes, int cmd, …/*一个整数或者记录指针*/);

实现功能:

(1). 复制一个现有文件描述符cmd = F_DUPFD;新文件描述符作为函数值返回

(2). 获得/设置文件描述符标记cmd = F_GETFD或F_SETFD;注意:使用F_SETFD和SETFL时必须要谨慎。需要先获得当前值根据需要来修改它。

(3). 获得/设置文件状态标识cmd = F_GETFL或F_SETFL; F_GETFL对应的文件标识作为函数值返回。O_开头的(open函数部分有说明)。三个访问标识位O_RDWR,O_WRONLY,O_RDONLY并不各占一位,他们互斥。需要用屏蔽字O_ACCMODE取得访问模式位再与其中一种作比较。F_GETFL将文件部分设置为第三个参数。设置文件状态标识时常用或|运算,~取补运算。例如当前若干设置为flags|=O_APPEND;等。首先获得当前值,int val; (val=fcntl(fd, F_GETFL, 0) ) >= 0, 设置flags相关选项为val|=flags; 关闭flags相关选项为val&=~flags;

如果设置了O_SYNC那么每次write都要等待直到数据写到硬盘再返回。数据库系统需要这种机制以确认write返回时数据确定写到磁盘上,以免系统崩溃时数据丢失。但是会大大增加系统时钟时间。

(4). 获得/设置异步I/O所有权cmd = GETOWN后SETOWN;获取/设置当前SIGIO和SIGURG信号的进程ID和进程组ID。

(5). 获得/设置记录所cmd = F_GETLK, F_SETLK或者F_SETLKW;

  1. 		#include "apue.h"  
  2.  
  3. #include <fcntl.h>  
  4.  
  5.   
  6.  
  7. int  
  8.  
  9. main(int argc, char *argv[])  
  10.  
  11. {  
  12.  
  13.   int    val;  
  14.  
  15.   
  16.  
  17.   if (argc != 2)  
  18.  
  19.     err_quit("usage: a.out <descriptor#>");  
  20.  
  21.   
  22.  
  23.   if ((val = fcntl(atoi(argv[1]), F_GETFL, 0)) < 0)  
  24.  
  25.     err_sys("fcntl error for fd %d", atoi(argv[1]));  
  26.  
  27.   
  28.  
  29.   switch (val & O_ACCMODE) {  
  30.  
  31.     case O_RDONLY: printf("read only"); break;  
  32.  
  33.     case O_WRONLY: printf("write only"); break;  
  34.  
  35.     case O_RDWR: printf("read write"); break;  
  36.  
  37.     default: err_dump("unknown access mode");  
  38.  
  39.   }  
  40.  
  41.   
  42.  
  43.   if (val & O_APPEND)  
  44.  
  45.      printf(", append");  
  46.  
  47.   if (val & O_NONBLOCK)  
  48.  
  49.     printf(", nonblocking");  
  50.  
  51. #if defined(O_SYNC)  
  52.  
  53.   if (val & O_SYNC)  
  54.  
  55.     printf(", synchronous writes");  
  56.  
  57. #endif  
  58.  
  59. #if !defined(_POSIX_C_SOURCE) && defined(O_FSYNC)  
  60.  
  61.   if (val & O_FSYNC)  
  62.  
  63.     printf(", synchronous writes");  
  64.  
  65. #endif  
  66.  
  67.   putchar('n');  
  68.  
  69.   exit(0);  
  70.  
  71. }  

 

lrwx------   1 user user 64   6   2 18:17 0 -> /dev/null

l-wx------   1 user user 64   6   2 18:17 1 -> pipe:[280311]

lrwx------   1 user user 64   6   2 18:17 2 -> /dev/pts/0

lr-x------   1 user user 64   6   2 18:17 3 -> /proc/22698/fd

    标准输入被重定向到/dev/null,说明ls命令不用接受键盘输入。

标准输出指向一个管道文件,可以想见,ls命令的输出通过管道交由watch命令处理。

标准错误输出没有重定向,仍然指向我们远程登录的终端.

fd-3指向文件/proc/22698/fd

其中pipe后面的数字和/proc/后面的数字是不断地增大的,为什么?这个容易理解,watch命令不断地重新调用ls命令,新的ls命令的进程号不断地变大;用来接收ls命令输出的管道也自然每次都是新的,为文件的node number(Linux内核的inode)。

检验:lsof指令:

在UNIX® 环境中,文件无处不在,这便产生了一句格言:“任何事物都是文件”。通过文件不仅仅可以访问常规数据,通常还可以访问网络连接和硬件。在有些情况下,当您使用ls 请求目录清单时,将出现相应的条目。在其他情况下,如传输控制协议(TCP) 和用户数据报协议(UDP) 套接字,不存在相应的目录清单。但是在后台为该应用程序分配了一个文件描述符,无论这个文件的本质如何,该文件描述符为应用程序与基础操作系统之间的交互提供了通用接口。

COMMAND    PID   USER   FD   TYPE        DEVICE SIZE/OFF      NODE NAME

sched        0   root  cwd   VDIR         136,8     1024         2 /

init         1   root  cwd   VDIR         136,8     1024         2 /

vi        2013   root    3u  VREG         136,8        0      8501 /var/tmp/ExXDaO7d

进程名称     PID    所有者  文件描述符(cwd当前目录,数组标识当前文件描述符fd)  ,类型可以是CHR,BLK等筛选选项:根据fd列筛选-d 根据pid筛选-p 多个条件应加逻辑运算符-a -o等   (逻辑或-o是默认设置)。已知文件pathname查找信息lsof /file/file1

       恢复删除的文件(如果这个文件被某个进程打开)lsof | grep server1.out第二个字段得到进程pid,第四个字段得到文件在该进程中的fd号然后/proc/<pid>/<fd>/

打开文件/dev/fd/n等效于复制描述符n。例如:fd = open(“/dev/fd/0”, mode); 此时大多数系统都会忽略指定的mode,等效于fd=dup(0);所以描述符0和fd共享同一文件表项,其文件状态/访问权限和原有权限相同。某些系统提供路径名/dev/stdin、/dev/stdout和/dev/stderr。

相关热词搜索: Unix 文件