开源中文网

您的位置: 首页 > FreeBSD > 正文

freebsd系统编程[简体中文版]5

来源:  作者:

l_start 

这是一个相对于l_whence,单位为字节的偏移量,换句话说,要求的位置实际上是l_whence + l_start 

l_len 

需设置为期望位置的长度,单位为字节,锁将从l_whence + l_start开始锁定l_len字节,如果你想整个文件用一把锁,那么设定l_len的值为0,如果l_len的值是一个负数,结果是不可预测的。 

l_pid 

需要设置为工作在锁上的进程的ID 

l_type 

需要设置为期望的锁的类型,下面是能够使用的值 

    * F_RDLCK - 读锁定 
    * F_WRLCK - 写锁定 
    * F_UNLCK - 用作清除锁定 

l_whence 

这是这个系统调用里面最混乱的部分,这个字段将决定l_start位置的偏移量,需要设为: 
    * SEEK_CUR - 在当前位置 
    * SEEK_SET - 在文件开始 
    * SEEK_END - 在文件末尾 

fcntl的命令 

下面的可用作fcntl的命令 

#define  F_GETLK      7

F_GETLK 命令尝试检查否能上锁,当使用这个命令,fnctl将检查是否有相冲突的锁,如果存在相冲突的锁,fnctl将改写flock结构,用冲突锁消息通过检查,如果没有相冲突的锁,那么在flock结构最初的信息将被保留,除非l_type字段被设成F_UNLCK 
#define  F_SETLK      8

F_SETLK命令试图获得flock结构描述的锁,如果锁不被承认,本次调用将不被阻塞。不管怎样,fcntl将直接返回EAGAIN,同时将设置相应的errno,当flock结构的l_type被设置为F_UNLCK时,你能够使用这个命令清除一个锁。 
#define  F_SETLKW     9

F_GETLK命令试图获得flock结构描述的锁,它将命令fnctl阻塞,直到赋予一个锁 

5.4为什么用FLOCK   

对大部分情况来说,高级文件锁定机制是有好处的。然而,POSIX.1接口有几个缺点。第一,当一个文件的任何一个文件描述符被关闭时,与该文件关联的所有锁定必须被删除。换句话说,如果你有一个进程打开一个文件,接着它调用一个函数打开同一个文件,读取然后关闭它,这样先前你对这个文件的所有锁定都会被删除。如果你并不确定一个库例程会做什么,这将会引起严重问题。第二,锁定是不会传递给子进程的,所以一个子进程必须独立地创建属于它自己的锁定。第三,所有在一个exec调用之前获得的锁定在由exec启动的进程释放,关闭这个文件或结束之前不会被释放。所以,如果你需要锁定文件的某一部分,那么调用exec不需要释放锁定或者关闭文件描述符,那部分区域将被锁定直到进程结束,你想要的或许不是预期的结果,不论如何,BSD的设计者用许多flock创建了非常简单的优先级文件上锁的接口. 

flock用于锁定整个文件,是BSD优先选择的方法,跟fcntl高级锁定相反,flock机制允许锁定进入子进程,使用flock调用的其他好处是可以在文件级并且不在文件描述符级别完成锁定,在某些情况下可用优先选择,意味着多个参照同一个文件的文件描述符,例如DUP()调用,或者多个OPEN()调用,希望每一个参照相同的文件锁,带flock的文件锁跟一个写多个读的fcntl锁很类似,不论如何,当下面的操作被定义,调用flock时,锁的优先级能够被提升: 
#define   LOCK_SH        0x01      /* shared file lock */

LOCK_SH操作用于在文件上(类似fcntl的读锁)创建共享锁,在一个文件上,多个进程能够共享一把锁。 
#define   LOCK_EX        0x02      /* exclusive file lock */

LOCK_EX操作用于在文件上创建互斥锁,当互斥锁生效时,文件上不能存在其他的共享锁,包括已共享的锁。 
#define   LOCK_NB        0x04      /* don't block when locking */

使用这个,flock的调用将阻塞,直到锁定生效,不论如何,假如期望的操作LOCK_NB是ORed,flock的调用将向EWOULDBLOCK返回错误号成功(0)或者失败(1). 
#define   LOCK_UN        0x08      /* unlock file */

LOCK_UN用于移除文件上的锁 

通过调用期望的flock,flock锁的优先级能够被提升或者降低。新的成功的调用将使最近生效的锁替换先前的锁。 

DUP函数 
int dup(int old);

像fcntl调用能够用作为现有的文件描述符复制描述符一样,dup函数也能复制文件描述符,dup调用能够返回一个跟old参数无法区别的新的文件描述符,这就意味着所有的read(),write()跟lseek()调用都会操作两个描述符(复制出来的新的跟原来的描述符),同时,所有fcntl的选项将保留,close on exec位出外, close on exec位会被关闭,所以你可以复制一个文件描述符,然后允许子进程去调用一个exec函数,这是dup函数非常普遍的一个用法。old参数用作复制,并且必须是一个有效的参数,描述目标描述符,新文件符至少应该是没使用过的文件描述符,它由成功调用dup函数后返回。它意味着如果你关闭STDIN_FILENO(该值为0)那么dup直接调用的新文件描述符的值将是STDIN_FILENO,假如dup函数调用因一些原因失败,将返回-1并且相应地设置errno. 

DUP2函数 
int dup2(int old, int new);

除了new参数是希望的目标值,dup2函数跟dup函数很相似。假如新参数已经参照了一个有效打开的描述符,并且它的值跟老参数的值不一样,那么新文件描述符将先被关闭。假如新参数等于老参数,那么函数不会做任何操作。成功调用DUP2的返回值将等于新参数。假如DUP2调用失败,将返回-1并且相应地设置errno. 

5.5  进程间通信 

来自于System V的主要的功能是基本的进程间通信,或者说IPC,这些一直在BSD里面被非常广泛地使用。IPC机制允许程序之间相互共享数据。这个很像我们已经介绍过的重定向,但是重定向是单向处理而不是双向处理。在示范程序中,重定向可以跟设置过STDIN_FILENO参数的CAT命令共享数据,但是有一个问题:cat命令不能跟重定向的程序共享数据,假设一个算法是从其他文件描述符读但是用open不灵活的时候,我们可以修改他们使其可以双向地相互共享数据。BSD提供了很多更好的用于进程间通信的方法。 

PIPE函数 
int pipe(int *array);

通过给定一个有效的二维数组调用pipe函数(如:int array[2]),pipe函数将分配两个文件描述符,假如成功,数组将包含两个有区别的并且允许单向通信的文件描述符, 打开的第一个文件描述符(array[0])用于读,打开的另一(array[1])个用于写,所以,自从成功调用pipe函数后,你能在这两个描述符之间得到一个单向通信通道,当写入其中一个时,你也将能够从另一个读取输出,管道函数比重定向好的地方是你不必使用文件, 

这些文件描述符的行为与普通的文件描述符完全一样,然而它们没有任何文件与之关联,管道功能使unix shell加到其他命令的管道变得很有用,例如 

bash___FCKpd___10nbsp;find /  -user frankie  | grep -i jpg  | more


这个例子里面,find命令将其输出通过管道传送给grep命令,grep再将其输出通过管道传送给more命令。当建立这个顺序的时候,shell将处理管道实际的设置,这些程序本身没有写到其他程序的想法,因为他们真的不需要知道。从这个例子你能看见自从调用了管道,正常来说进程就会fork, 之后进程就能通信,假如要达到双向通信的目标,你需要创建两个管道,一个用于父进程到子进程的通信,另一个用作子进程到父进程的通信。 

管道有下面两条通信规则: 
    1.如果管道读的这边关闭了,试图写到这个管道将会导致一个SIGPIPE信号发送给试图写的进程。 
    2.如果管道写的这边关闭了,试图从这个管道读将导致读返回0或文件结束。关闭写端是发送文件结束到该管道读端的唯一方法。 

成功地调用pipe函数后将返回0,假如调用失败,将返回-1并且errno将被相应地设置, 

备注:在更多的现代的BSD上,单个描述符上pipe函数支持双向通信,不论如何,这个特性不是很轻便,也因为如此,这个方式不建议使用。 


Mkfifo函数 

  
int mkfifo(const char *path, mode_t mode);


在相关的进程间通信时,管道是有用的,然而在没有关联关系的进程间通信时,使用mkfifo函数。Mkfifo函数实际上在文件系统创建一个文件。这个文件只是其他文件在通信时使用的一个标志。这些文件就叫做FIFO管道(先进先出管道)。当一个进程创建一个FIFO管道,写到这个FIFO管道并不会写到这个文件,而是被另一个进程读取。这种行为与管道非常相似,所以FIFO也被叫做命名管道。Mkfifo函数有两个参数。第一个参数是一个以null结尾的字符串,声明路径和文件名。第二个参数是该文件的存取模式。该存取模式是标准的unix文件所有者读写权限(参考/usr/include/sys/stat.h中的S_IRUSR, S_IRGRP等)。 

一旦mkfifo函数成功调用,需要用open函数打开创建的fifo管道进行读写。如果调用失败,会返回-1而且错误代码会被相应地设置。 

创建fifo管道与创建文件相类似,进程也必须有足够的权限来创建fifo管道,因为该fifo的用户ID会被设置为该进程的有效用户ID,而组ID会被设置为该进程的有效组ID。 

关于FIFO重要的一点是,在缺省情况下,它们是阻塞的。因此,读取一个fifo管道会被阻塞,直到另一端把数据写进来,反之亦然。为了避免这种情况,可以使用O_NOBLOCK参数来打开。在这种情况下,你会获得以下行为:对读的调用会立即返回,返回值为0;或者对写的调用会导致一个SIGPIPE信号。 

5.6消息队列 

另一种进程间通信机制,消息队列,为进程间通信提供另一种方式。然而和我提到的其他通信机制不同,你应该尽量避免使用这种方式。如果你的程序使用消息队列,试着用fifo或者甚至Unix Domain Socket重新实现它。在我讨论原因之前,以下是一个快速概述。 

消息队列与FIFO相似,但它们使用一个键而不是一个文件作为引用。这个键是一个无符号整数。一旦一个消息队列被创建,发送到这个消息队列的数据会被内核缓存。内核分配给消息队列的内存是有限的。一旦一个消息队列被填满,那么在一个进程从这个消息队列把数据读走之前不能再往这个队列发送数据。在这种情况下,如果两个进程以不同的速度读写,队列是可靠的而且在大多数情况下是非堵塞的。这与FIFO不同。在FIFO的机制中,一个慢的读进程事实上是会拖慢一个比较快的写进程的(除非设置了O_NONBLOCK参数)。另一个好处是即使写进程退出了,写到消息队列中的数据也会被保存起来直到另一个进程读取它。而在FIFO的机制下,如果写进程退出了,这个FIFO会被关闭,而读进程会收到一个文件结束的标志。 

以上这些所有的消息队列的好处看起来不错,但让我们再仔细看看。假定一个进程打开一个消息队列,往里面写了一大块数据,填满了内核的缓冲区然后退出,内核将不得不保存这些数据直到另一个进程来读取它,而其他任何想创建和写消息队列的进程将会被拒绝。这种情况将会持续直到一个进程读走这些数据或者系统重启。在这种情况下,创建一个简单的针对消息队列的拒绝服务是可能的。 

另一个问题是键并不确保是唯一的。换句话说,一个进程无法确立一种方式来确定它是否是使用一个特定消息队列的唯一进程。而在FIFO机制中,当一个进程创建一个FIFO时,它有比较好的机会知道这个FIFO是否唯一因为可以声明一个针对文件路径的预先约定(比如:/usr/local/myapp/fifo_dir)。你的应用程序可以在安装时创建一个唯一的目录,这样几乎可以确保一个唯一的FIFO路径。有效的消息队列键值可以通过调用一个函数ftok来生成以帮助减少相同的键值,但并不能确保唯一。这个问题的副作用难于确定—你的程序可能读到它并不想读的数据,或者你的程序写入的数据正被其他并不想读取的进程读取。简短来说,当你使用消息队列时,可能导致难于调试的奇怪行为和错误。 

如果你仍然坚持使用消息队列,参考以下的指南页:ftok(3), msgget(3), msgctl(3), msgrcv(3), 和msgsnd(3)。 

5.7 结论 

这一章介绍了几个操作打开文件描述符的系统调用,包含在我们fork和exec之前必须关闭文件描述符的场景。我们也讨论了文件锁定,设置和移除文件锁定,以及一些特殊的文件描述符如FIFO和Queue等,在那种情况下根本不在文件系统上存储数据。这些系统调用为BSD增加了很大的可编程性和灵活性。但当一个进程有多个打开的文件描述符时会怎么样?下一章将会讨论高效地处理多个文件描述符。 



[align=center]第六章 高级I/O[/align] 
[align=center]翻译:雨丝风片@chinaunix.net[/align] 

6.1 高级I/O和进程资源 

正如我们在前面章节中看到的,程序可以同时打开多个文件描述符。这些文件描述符并不一定就是文件,还可以是fifo、pipe或者socket。于是,如何复用这些打开的描述符就很重要了。例如,考虑一个简单的邮件阅读程序,比如pine。它显然应当允许用户在读写email的同时也能去检查是否有新邮件。这就意味着在任一给定时刻都至少能够接收两个来源的输入:一个来源是用户,另一个是用来检查新邮件的描述符。处理描述符的复用是个复杂的问题。一种方法是把所有打开的描述符都标记为非阻塞的(O_NONBLOCK),然后在它们之中循环,直到找到一个可以进行I/O操作的描述符为止。这种方法的问题是程序会一直在循环,如果长时间内没有I/O可用,进程就会一直占据CPU。当有多个进程在一组很少的描述符上循环时,你的CPU的负载就会恶化。 

另一种方法就是设置信号处理器去捕获I/O变为可用的事件,然后就让进程进入休眠状态。如果你只打开了少量的描述符,而且并不经常请求I/O的话,这种方法从理论上看倒是不错。由于进程已经休眠,就不会再占用CPU,仅当I/O可用时它才恢复执行。然而,这种方法的问题在于信号处理的开销有点大。比如一个web服务器,每分钟收到100个请求,那就几乎一直都在捕获信号。每秒钟捕获上百个信号的开销是相当大的,不单是进程,对于内核发送信号的开销而言也是一样的。 

到目前为止,我们看到的两种选择都有限制,效率也不高,它们需要解决的共同问题就是进程需要知道I/O究竟什么时候能用?然而,这个信息实际上只有内核才能事先知道,因为是内核在最终处理系统中的所有打开的描述符。例如,当一个进程通过fifo向另一个进程发送数据的时候,发送进程会调用write,这是一个系统调用,因此会进入内核。在发送方的write系统调用执行完毕之前接收方对此是一无所知的。于是就引出了一个更好的复用文件描述符的方法:由内核来替进程管理描述符。换句话说,就是把一个打开描述符的链表发送给内核,然后等待,直到内核发现某个或多个描述符已经准备好了或者已经超时了为止。 

这就是select()、poll()和kqueue()接口采用的方法。通过这些接口,内核就会管理文件描述符,当I/O可用时就去唤醒进程。这些接口巧妙地处理了上述问题。进程不必再在打开的文件描述符中循环,也不必再去设置信号了。但进程在使用这些函数的时候还是会产生一点小问题。这是因为I/O操作是在从这些接口返回之后才去执行的。所以它至少需要两个系统调用才能完成其操作。例如,你的程序有两个用于读的描述符。你对它们使用select,然后等待它们直至有数据可读。这就需要进程首先调用select,在select返回之后,就对该描述符调用read。更妙的是,你还可以对所有打开的描述符执行一个整体的read。一旦其中有某个描述符准备好读之后,read就会返回,并把数据放在缓冲区中,同时还会给出一个标识,用来指示这个数据是从哪个描述符读进来的。 

6.2 select 

我首先要讲的接口是select()。格式如下: 

int  select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds,  struct timeval *timeout);


传给select的第一个参数已经造成了多年的混乱。nfds参数的正确用法是把它设成文件描述符的最大值加1。换句话说,如果你有一组文件描述符{0,1,8},nfds参数就应当被设置成9,因为你的描述符的最大值为8。有些人错误地以为这个参数的意思是文件描述符的总数加1,对于我们的例子而言就是4。记住,一个文件描述符只是一个整数而已,所以你的程序就需要指出你所想要在其上select的最大的描述符值。 

select接下来会按顺序针对所有尚未完成的读、写以及异常条件检查其余的三个参数,readfds、writefds和exceptfds。(详细信息请参见man(2) select)。注意,如果readfds、writefds和execptfds中没有设置描述符,那么传给select的对应参数应当被设置成NULL。 

readfds、writefds和execptfds参数通过以下4个宏进行设置。 

FD_ZERO(&fdset); 

FD_ZERO宏用来对指定的描述符集合中的bit进行清零。有一点需要特别注意:只要使用select,就应当调用这个宏;否则select的行为将是不可预知的。 

FD_SET(fd, &fdset);  

FD_SET宏用于向一组激活的描述符中添加一个描述符。 

FD_CLR(fd, &fdset);  

FD_CLR宏用于从一组激活的描述符中删除一个描述符。 

FD_ISSET(fd, &fdset); 

FD_ISSET宏是在select返回之后使用的,用于测试某个描述符是否已准备好进行I/O操作。 

select的最后的参数是一个超时值。如果超时值被设置为NULL,则对select的调用将以不确定的方式被阻塞,直至某个操作已准备好为止。如果你需要一个确定的超时时间,那么超时值就得是一个非空的timeval结构体。timeval结构体如下: 

  struct timeval {

      long    tv_sec;         /* seconds */

      long    tv_usec;        /* microseconds */

  };


如果select调用成功,将返回准备好的描述符的数目。如果select因为超时而返回,则返回值为0。如果有错误发生,则返回-1,同时会相应地设置errno。 

6.3 poll 

我们在这里对I/O的讨论主要是针对BSD的。System V支持一种特殊类型的I/O,即所谓的STREAMS。和socket一样,STREAMS也具有优先级属性,这种属性有时也被成为数据带。数据带可用来给STREAMS中的特定数据设置较高的优先级。BSD最初并不支持这一特性,不过有些人添加了System V仿真功能,可以对某些类型提供支持。由于我们并不关注System V,因此我们只会引用数据带或数据优先级带的概念。详细信息请参见System V STREAMS。 

poll函数和select很相似: 

  int  poll(struct pollfd *fds, unsigned int nfds, int timeout);


和原产于BSD的select不同,poll是由System V Unix创建的,在早期的BSD版本中并不支持它。目前主流BSD系统中都已经支持poll了。 

和select相似,poll也是在一组给定的文件描述符上进行复用。在指定这些描述符的时候,你必须使用一个结构体数组,其中每个结构体代表一个文件描述符。和select相比,poll的好处就是你可以判断一些很罕见的条件,而select则无法做到。这些条件是POLLERR、POLLHUP和POLLNVAL,我们稍后讨论。尽管对于选择select还是poll的问题已经有了相当多的讨论,但这在很大程度上还是取决于你的个人爱好。poll所使用的结构体是pollfd结构体,如下: 

  struct pollfd {

      int     fd;             /* which file descriptor to poll */

      short   events;         /* events we are interested in */

      short   revents;        /* events found on return */

  };


fd 

fd成员用于指定你想要poll的文件描述符。如果你想删除一个描述符,那就把那个描述符的fd成员设置成-1。通过这种方法,你可以避免对整个数组进行混洗,同时还可以清除revents成员中列出的所有事件。 

events, revents  

events成员是一个bit掩码,用于指定针对指定描述符所关心的事件。revents成员也是一个bit掩码,但它的值是由poll设置的,用于记录在指定描述符上发生的事件。这些事件的定义如下: 

  #define POLLIN          0x0001


POLLIN事件表明你的程序将选择该描述符上的可读数据事件。注意,此处的数据不包括高优先级数据,比如socket上的带外数据。 

  #define POLLPRI         0x0002


POLLPRI事件表明你的程序准备选择该描述符上的任何高优先级事件。 

  #define POLLOUT         0x0004

  #define POLLWRNORM      POLLOUT


POLLOUT和POLLWRNOMR事件表明你的程序想知道什么时候可以对一个描述符执行写操作了。在FreeBSD和OpenBSD上这两个事件是相同的;你可以在你的系统头文件(/usr/include/poll.h)中查证这一点。从技术角度来说,它们之间的区别在于POLLWRNOMR仅当数据优先带等于0的时候才去检测是否可以进行写操作。 

  #define POLLRDNORM      0x0040


POLLRDNORM事件表明你的程序准备选择该描述符上的常规数据。注意,在某些系统上,这个事件指定的操作和POLLIN完全一样。但在NetBSD和FreeBSD上,这个事件和POLLIN并不相同。同样,请去查看你的系统头文件(/usr/include/poll.h)。严格地说,POLLRDNORM仅当数据优先带等于0的时候采取检测是否可以进行读操作。 

  #define POLLRDBAND      0x0080


POLLRDBAND事件表明你的程序想知道什么时候能够以一个非0的数据带值从该描述符读数据。 

  #define POLLWRBAND      0x0100


POLLWRBAND事件表明你的程序想知道什么时候能够以一个非0的数据带值向该描述符写数据。

Tags:freebsd 系统 编程
关于开源中文网 - 联系我们 - 广告服务 - 网站地图 - 版权声明