第十四章 非阻塞I/O 和 记录锁

本文探讨了非阻塞I/O的工作原理及其实现方式,通过具体示例展示了如何利用非阻塞I/O进行高效的数据读写。此外,还深入介绍了记录锁的概念及其在防止文件冲突中的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

本章包括:

(非阻塞I/O、 记录锁、 I/O多路转接(select和poll函数)、 异步I/O、 readv和writev函数和存储映射I/O(mmap))



非阻塞I/O
    之前章节中将系统调用分为两类
        1、低速系统调用
        2、其他
    低速系统调用是可能会使进程永远阻塞的一类系统调用。
    我们可以通过以下两种方法使得这些操作不会永远阻塞: 如果这种操作(调用)不能完成,即该操作若继续执行则将阻塞,那么调用立即出错返回。
        A、如果调用open获得描述符,则可以指定 O_NONBLOCK标志
        B、对于一个已经打开的描述符,则可以调用 fcntl, 由该函数打开 O_NONBLOCK标志。
    
    下面的程序是非阻塞I/O的实例。从标准输入读入 500000字节,并试图写到标准输出上。

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>

char buf[500000];

void set_fl(int s, int f)
{
        int ori = fcntl(s,F_GETFL,0);
        ori |= f;
        fcntl(s,F_SETFL,ori);
}

void clr_fl(int s ,int f)
{
        int now = fcntl(s,F_GETFL,0);
        now &= ~f;
        fcntl(s,F_SETFL,now);
}

int main()
{
        int ntowrite, nwrite;
        char *ptr;

        ntowrite = read(STDIN_FILENO,buf,sizeof(buf));        //从标准输入读入
        fprintf(stderr, "read %d bytes\n",ntowrite);        //将这句话输出到标准错误输出

        set_fl(STDOUT_FILENO, O_NONBLOCK);                //打开O_NONBLOCK标志

        ptr = buf;
        while(ntowrite > 0)
        {
                errno = 0;
                nwrite = write(STDOUT_FILENO,buf,ntowrite);
                fprintf(stderr,"nwrite = %d, errno= %d\n",nwrite,errno);    //每次将这句话输出到标准错误输出

                if(nwrite > 0)
                {
                        ptr += nwrite;
                        ntowrite -= nwrite;
                }
        }

        clr_fl(STDOUT_FILENO,O_NONBLOCK);                //关闭O_NONBLOCK标志

        return 0;
}



    输出结果见书 P390

   
    当我们将标准输出指定为文件时,write只执行一次(还记得第三章的write部分吗,在目标是文件的时候是全缓冲,目标是终端则是行缓冲)
        即 ./a.out < /etc/services > temp.file        //从 services读取500000字节,并将标准输出至普通文件
    当我们将标准输出指定为终端, 且指定错误输出至stderr.out文件:
        即 ./a.out < /etc/services 2>stderr.out
        write有时返回小于500000的数字,有时则返回错误    P390
        这种形式的循环称为轮询,在多用户系统上用它会浪费CPU时间。





记录锁
    记录锁的功能是:
        当第一个进程正在读或修改文件的某个部分时,使用记录锁可以阻止其他进程修改同一文件区
fcntl记录锁
   
函数    fcntl(int fd, int cmd, .../*struct flock* flockptr/)
        对于记录锁, cmd可以是 F_GETLK, F_SETLK, F_SETLKW ; 第三个参数是一个指向 flock 结构的指针

struct flock
{
    short l_type;        //F_RDLCK(共享读锁), F_WRLCK(独占写锁), F_UNLCK            即锁类型
    short _whence;        //SEEK_SET, SEEK_CUR, SEEK_END            要加锁或解锁的起始字节偏移量
    off_t l_start;        //offset in bytes, relative to l_whence    
    off_t l_len;        //length, in bytes; 0 means lock to EOF    区域字节长度; 若为0,可扩展到最大可能偏移量
    pid_t l_pid;        //returned with F_GETLK                进程的ID持有的锁能阻塞当前进程
};


    任意多个进程在一个给定的字节上可以有一把共享的读锁,会拒绝后来的写锁
    但是在一个给定字节上只能有一个进程有一把独占写锁,拒绝后来的写锁和读锁
        以上规则适用于不同进程之间,并不适用于单个进程。
        如果一个进程对一个文件区间已经有了一把锁,后来该进程又企图在同一文件区间再加一把锁,那么新锁将替换已有锁。

    加读锁时,描述符必须是读打开; 写锁时则必须写打开

    下面说明fcntl中用于锁的cmd可用的3种:
        F_GETLK
            判断由 flockptr 所描述的锁是否会被另外一把锁排斥(阻塞)。若存在这样一把锁阻止创建由flockptr所描述的锁,则该现有锁的信息将重写flockptr指向的信息。如果不存在这样的情况,则除了将l_type设置为F_UNLCK之外,flockptr指向的信息不变。
        F_SETLK
            设置由 flockptr 所描述的锁。
        F_SETLKW
            如果所请求的读锁或写锁因另一个进程当前已经对所请求的区域的某部分进行了加锁而不能授予,那么调用进程会休眠。若请求的锁已经可用或是休眠被信号打断,则该进程被唤醒。
    
    需要注意的是,用 F_GETLK测试能否建立一把锁,然后用F_SETLK或F_SETLKW企图建立那把锁,这两者并不是一个原子操作。因此不能保证是否会有进程在空窗期建锁。

    在设置或释放文件上的一把锁时,系统按要求组合或分裂相邻区。
        例如,若100-199字节是加锁区,需解锁第150字节,则内核将维持两把锁,一把锁 100-149 ,另一把则锁 151-199.
            假若我们又对150加锁了,那么系统又会把3个加锁区域合并为一个区

    以下为加锁与测试锁的宏:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>

//-----------------------------to set a lock---------------------------------------------------------------------------

int lock_reg(int fd, int cmd, int type, off_t offset, int whence, off_t len)
{
        struct flock lock;

        lock.l_type = type;
        lock.l_whence = whence;
        lock.l_len = len;
        lock.l_start = offset;

        return fcntl(fd,cmd,&lock);
}

//set a read lock
#define read_lock(fd,offset,whence,len) lock_reg((fd),F_SETLK,F_RDLCK,(offset),(whence),(len))

//set a reat wait lock
#define readw_lock(fd,offset,whence,len) lock_reg((fd),F_SETLKW,F_RDLCK,(offset),(whence),(len))

//set a write lock
#define write_lock(fd,offset,whence,len) lock_reg((fd),F_SETLK,F_WRLCK,(offset),(whence),(len))

//set a write wait lock
#define writew_lock(fd,offset,whence,len) lock_reg((fd),F_SETLKW,F_WRLCK,(offset),(whence),(len))

//unlock the lock
#define un_lock(fd,offset,whence,len) lock_reg((fd),F_SETLK,F_UNLCK,(offset),(whence),(len))



//--------------------------------to test a lock----------------------------------------------------------------------

pid_t lock_test(int fd, int type, off_t offset, int whence, off_t len)
{
        struct flock lock;

        lock.l_type = type;
        lock.l_whence = whence;
        lock.l_len = len;
        lock.l_start = offset;

        if(fcntl(fd,F_GETLK,&lock) < 0)
        {
                perror("fcntl");
                exit(1);
        }

        if(lock.l_type == F_UNLCK)              //if it has no lock,then just turn the type into F_UNLCK
                return 0;

        return lock.l_pid;                      //if it already has a lock, then change the lock
}

//if can be added a read lock
#define is_read_lockable(fd,offset,whence,len) (lock_test((fd),F_RDLCK,(offset),(whence),(len)) == 0)

//if can be added a write lock
#define is_write_lockable(fd,offset,whence,len) (lock_test((fd),F_WRLCK,(offset),(whence),(len)) == 0)



    要注意的是,不能用测试锁函数用于测试自己的锁是否存在,因为调用进程绝不会阻塞在自己的锁上。F_GETLK也绝不会报告调用进程自己持有的锁。

    还有一个内容是会造成死锁。
    检测到死锁后,内核必须要选择一个进程接收出错返回,哪一个依赖于系统实现。


锁的隐含继承和释放
    1、锁与进程和文件两者相关联。
        当一个进程终止时,它所建立的锁全释放;
        无论一个描述符何时关闭,该进程通过这一个描述符引用的文件上的任何一把锁都会释放(当然,这些锁是该进程设置的),意味着:

fd1 = open(pathname, ...);
read_lock(fd1, ...);
fd2 = dup(fd1);
close(fd2);

则在 close(fd2)之后,在fd1上设置的锁被释放。 如果将dup替换为open ,其效果也一样

fd1 = open(pathname, ...);
read_lock(fd1, ...);
fd2 = open(pathname, ...);
close(fd2);
    
    2、由fork产生的子进程不继承父进程所设置的锁。
    3、在执行exec后, 新程序可以继承原执行程序的锁。 但是,如果对一个文件描述符设置了执行时关闭,那么当作为exec的一部分关闭该文件描述符时,将释放相应文件的所有锁。

    关于锁,部分跳过         P397

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值