APUE读书笔记-高级I/O

时间:2022-03-08 17:50:23

高级I/O

1,非阻塞I/O


    有些调用可能会使进程永远阻塞,它们可能包括:
    .读管道,终端设备或网络设备的数据并不存在时,读操作会使调用者永远阻塞。 
    .数据不能立即被写到上述同类型文件接受,该写操作也会阻塞。
    .在某条件发生之前,打开某种类型的文件,也会被阻塞,例如:打开用写模式打开FIFO,该FIFO没有任何其>他进程用读模式打开时,也会阻塞。
    .对已经加上强制性记录锁的文件进行读写。
    .某些ioctl操作
    .某些进程通信函数:

    对于一个给定的描述符有两种方法对其指定非阻塞I/O:
    (1)open(fd, O_NONBLOCK)。
    (2)对于一个已经打开的描述符,可以用fcntl,来设定非阻塞O_NONBLOCK标志。

 

2, 记录锁(文件锁)

*记录锁和文件锁
    .记录锁的上锁粒度可以很小,可以到字节;而文件锁是对整个文件上锁。
    .用fcntl上锁是针对进程间的,不是针对一个进程的线程的。

*记录锁
    当两个进程同时编辑一个文件时,其文件的最后状态是由最后一个进程编辑的状态决定的。而记录锁是为了当一个进程修改一个文件的同一区域时,它可以阻止其他进程修改同一文件区域或整个文件。
    给文件加锁的方式有很多中,进程间的加锁一般用,fcntl函数。                       
*建议性锁
    int fcntl(int fd, int cmd, /*struct flock *arg*/);   
    该函数是基于一个打开的描述符。该描述符的打开的属性决定了以后加锁的类型。
*用于记录上锁的cmd参数共有三个值。这三个命令要求第三个参数arg是指向某个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
                                 (F_GETLK only) */
             ...
         };
   
    F_SETLK
    获取一个锁(l_type是F_RDLCK或F_WRLCK)或者释放一个锁(l_type是F_UNLCK),该锁的范围基于l_whence,l_start,l_len的值。若该锁无法授与调用进程,该函数就立即返回一个EACCES或EAGAIN。
    F_SETLKW
    设置一个锁,和F_SETLK不同的是,如果由于原来有一把锁而不能设置,则进程等待到该锁释放,直到可以设置为止。如果在等待期间被一个信号终端,从该信号处理函数返回后,该函数也立即返回-1且设定errno=EINTR。   
    F_GETLK
    检查由arg指向的锁以确定是否有某个已存在的锁会妨碍新锁的授予。如果当前没有这样的锁存在,由arg指向的flock结构的l_type成员就被设置为F_UNLCK。否则,关于这个已存在锁的信息将在由arg指向的flock结构中返回,其中包括持有该锁的进程的进程ID。

*flock结构描述锁的类型,以及待解锁住的字符范围。和lseek一样,起始的字节偏移量作为一个相对偏移量(l_start成员)伴随其解释(l_whence成员)指定的。l_whence成员有三个取值:
    SEEK_SET :l_start 相对于文件的开头解释。   
    SEEK_CUR :l_start 相对于文件的当前字节偏移量解释。
    SEEK_END :l_start 相对于文件的末尾解释。
    l_len成员指定从该偏移量开始的连续字节数。长度为0表示,从起始偏移量到文件偏移量的最大可能值。

*锁住整个文件的方式
    (1)l_whence 成员为SEEK_SET,l_start 成员为0,l_len成员为0。   
    (2)用lseek把文件指针定位到文件头,然后指定l_whence成员为SEEK_CUR,l_start成员为0,l_len为0。

*加锁的原则
    对于文件的任意一个字节,最多只能存在一种类型的锁(读锁或写锁)。而且,一个给定字节可以有多个读出锁,但只能有一个写入锁。
       
*锁的隐含继承和释放
    .当一个进程终止时,它所建立的锁全部释放。   
    .任何时候关闭一个描述符时,该进程通过这一描述符可以引用的文件上的任何一把锁都被释放。
        fd1 = open(filename, ...);
        read_lock(fd1, ...);
        fd2 = dup(fd1);
        close(fd2);
    或
        fd1 = open(filename, ...);
        read_lock(fd1, ...);
        fd2 = open(filename, ...);
        close(fd2);
    在关闭close(fd2)后,在fd1上设置的锁被释放。   
    .由fork产生的子进程不继承父进程所设置的锁。
    .执行exec函数后,新程序可以继承原执行程序的锁,但若设置了close-on-exec标志,那么当作为exec的一部分关闭该文件描述符时,对应的文件的所有锁都被释放。

*记录锁和标准I/O
    记录上锁不应该同标准I/O函数一起使用,因为该函数库会执行内部缓冲。所以应该使用write或read。

*用宏来简化fcntl函数的调用
    加读锁       
    #define read_lock(fd, offset, whence, len) /
            lock_reg(fd, F_SETLK, F_RDLCK, offset, whence, len)
    加阻塞式读锁:
    #define readw_lock(fd, offset, whence, len) /
            lock_reg(fd, F_SETLKW, F_RDLCK, offset, whence, len)
    加写锁
    #define write_lock(fd, offset, whence, len) /
            lock_reg(fd, F_SETLK, F_WRLCK, offset, whence, len)
    加阻塞式写锁
    #define write_lock(fd, offset, whence, len) /
            lock_reg(fd, F_SETLKW, F_WRLCK, offset, whence, len)
    解锁       
    #define un_lock(fd, offset, whence, len)    
            lock_reg(fd, F_SETLK, F_UNLCK, offset, whence, len)   
    加锁测试:是否可以加一把读锁
    #define is_read_lockable(fd, offset, whence, len)
            !lock_test(fd, F_RDLCK, offset, whence, len)
    加锁测试:是否可以加一把写锁
    #defiine is_write_lockable(fd, offset, whence, len)
            !lock_test(fd, F_WRLCK, offset, whence, len)

用到的函数:   
int
lock_reg(int fd, int cmd, int type, off_t offset, int whence, off_t len)   
{
    struct flock lock;
   
    lock.l_type = type;     /* F_RDLCK, F_WRLCK, F_UNLCK */
    lock.l_start = offset; /* 相对于l_whence的偏移量 */
    lock.l_whence = whence; /* SEEK_SET, SEEK_CUR, SEEK_END */
    lock.l_len = len;    /* 0 means to EOF */

    return (fcntl(fd, cmd, &lock));    //-1 error
}

pid_t
lock_test(int fd, int type, off_t offset, int whence, off_t len)
{
    struct flock lock;
   
    lock.l_type = type;     /* F_RDLCK, F_WRLCK, F_UNLCK */
    lock.l_start = offset; /* 相对于l_whence的偏移量 */
    lock.l_whence = whence; /* SEEK_SET, SEEK_CUR, SEEK_END */
    lock.l_len = len;    /* 0 means to EOF */

    if (fcntl(fd, F_GETLK, &lock) == -1) //获取进程原来的锁,若成功把l_type设置成F_UNLCK
        return -1;
    if (lock.l_type == F_UNLCK)     //是否获取成功
        return 0;

    return(lock.l_pid);    //获取失败了,返回原来的进程ID
}

*劝告性锁
    若没有对文件的权限做处理,用fcntl加的锁都是建议性锁。它不能防止一个进程写已由另一个进程读锁定的某个文件。这样的程序vi是一个典型的例子,它为打开的文件加的就是劝告性锁,所以你编辑文件的时候别人也可以编辑,但结果是无法意料的。

*强制性锁(mandatory locking)   
    使用强制性锁后,内核检查每个read和write请求,以验证其操作不会干扰由某个进程持有的某个锁。
    .要使用强制性锁,需要满足的条件:
    (1) 组成员执行位必须关掉
    (2) SGID位必须打开
    注意:这样操作需要打开文件的组执行位,和用户执行位,这样才有意义。

*强制性锁的问题
    使用强制性锁仍然存在问题,主要是当有多个进程需要读或写一个文件时。在强制性锁的解锁的空档有可能让其他进程有机可乘,从而操作该文件,使得结果出现错误。

*记录锁的读锁和写锁的优先级
    .如果原来是读锁,现在有一个读锁和一个写锁的请求,那么一般会首先满足读锁让写锁等待。
    .如果原来是写锁,一般是顺序给予锁,按照FIFO的顺序。
    .看具体的系统而定,没有保证。

*锁的应用
    .守护进程中确保只有一个进程实例在运行。   
    write_lock(pidfd, 0, SEEK_SET, 0)        //开始给整个文件加上写锁,并保持
    .协同进程间的劝告性锁,也就是说需要有权限和逻辑上的控制,不能只靠锁。