fcntl和select函数彻底搞明白

32 篇文章 0 订阅

fcntl和select函数彻底搞明白

 

第一、fcntl函数详细使用      

fcntl有强大的功能,它能够复制一个现有的描述符,获得/设置文件描述符标记,获得/设置文件状态标记,获得/设置异步I/O所有权,获得/设置纪录锁。

当多个用户共同使用,操作一个文件的情况,Linux通常采用的方法就是给文件上锁,来避免共享资源产生竞争的状态。

fcntl文件锁有两种类型:建议性锁和强制性锁
         建议性锁是这样规定的:每个使用上锁文件的进程都要检查是否有锁存在,当然还得尊重已有的锁。内核和系统总体上都坚持不使用建议性锁,它们依靠程序员遵守这个规定。
         强制性锁是由内核执行的。当文件被上锁来进行写入操作时,在锁定该文件的进程释放该锁之前,内核会阻止任何对该文件的读或写访问,每次读或写访问都得检查锁是否存在。

        使用fcntl文件锁进行I/O操作必须小心:进程在开始任何I/O操作前如何去处理锁,在对文件解锁前如何完成所有的操作,是必须考虑的。如果在设置锁之前打开文件,或者读取该锁之后关闭文件,另一个进程就可能在上锁/解锁操作和打开/关闭操作之间的几分之一秒内访问该文件。当一个进程对文件加锁后,无论它是否释放所加的锁,只要文件关闭,内核都会自动释放加在文件上的建议性锁(这也是建议性锁和强制性锁的最大区别), 所以不要想设置建议性锁来达到永久不让别的进程访问文件的目的(强制性锁才可以)^_^;强制性锁则对所有进程起作用。

      可以用fcntl 函数改变一个已打开的文件的属性,可以重新设置读、写、追加、非阻塞等标志(这些标志称为File StatusFlag),而不必重新open 文件。
     #include<unistd.h>
      #include <fcntl.h>
      int fcntl(int fd, int cmd);
      int fcntl(int fd, int cmd, long arg);
      int fcntl(int fd, int cmd, struct flock *lock);


这个函数和open 一样,也是用可变参数实现的,可变参数的类型和个数取决于前面的cmd 参数。

文件锁包括了建议性锁强制性锁。

建议性锁要求每个上锁的文件的进程都要检查是否有锁存在,并且尊重已有的锁,在一般情况下,内核和系统都不使用建议性锁。

强制性锁是由内核执行的锁,当一个文件被上锁进行读写操作的时候,内核将阻止其他任何文件对其进行读写操作。每次读写操作都要检查是否有锁存在。

Linux中实现上锁的函数有lock()fcntl()

lock()
用于对文件施加建议性锁
fcntl()
用于对文件施加建议性锁和强制性锁都行。同时还可以对文件某一条纪录进行上锁,也就是记录锁。

记录锁分为读取锁(共享锁,它能够使多个进程都能在文件的同一部分建立读取锁)写入锁(排斥锁,在任何时刻只能有一个进程在文件的某部分建立写入锁。)。

fcntl
函数原型
#include<sys/types.h>
#include<unistd.h>
#include<fcntl.h>

int fcntl(int fd,   //
文件描述符
          int cmd , //
不同的命令
          struct flock *lock) //
设置记录锁的具体状态

cmd
取值:
F_DUPFD  
复制文件描述符
F_GETFD  
获得fdclose-on-exec标志
F_SETFD  
设置close-on-exec标志
F_GETFL  
获得open设置标志
F_SETFL  
设置lock描述的标志
F_GETLK  
测试该锁是否被另外一把锁排斥
F_SETLKW
如果存在其他锁,则调用进程睡眠,如果捕捉到信号则睡眠中断
F_GETOWN
检索收到的SIGIOSIGURG信号的进程号或者进程组号
F_SETOWN
设置进程号或进程组号

这里的lock结构体如下:
struct flock
{
    short l_type;   /*F_RDLCK(
读取锁),F_WRLCK(写入锁),F_UNLCK(解锁)*/
    off_t l_start; /*
相对偏移量(字节)*/
    short l_whence; /*SEEK_SET ,SEEK_CUR ,SEEK_END */
    off_t l_len;    /*
加锁区域长度*/
    pid_t l_pid;
}

成功:0
出错:-1
提示:如果加锁整个文件通常的方法是将l_start设置为0l_whence设置为SEEK_SET, l_len设置为0


      下面的例子使用F_GETFLF_SETFL这两种fcntl 命令改变STDIN_FILENO的属性上O_NONBLOCK 选项,实现非阻塞读终端的功能。

用fcntl改变File Status Flag


             #include <unistd.h>
            #include <fcntl.h>
            #include <errno.h>
            #include <string.h>
            #include <stdlib.h>
            #define    MSG_TRY "try again\n"
             intmain(void)
             {
                    char buf[10];
                    int n;
                    int flags;
                    flags = fcntl(STDIN_FILENO, F_GETFL);
                    flags |= O_NONBLOCK;
                    if (fcntl(STDIN_FILENO, F_SETFL, flags) == -1)
           
{
                            perror("fcntl");
                            exit(1);
                    }
            tryagain:
                    n = read(STDIN_FILENO, buf, 10);
                    if (n < 0)
                   {
                            if (errno == EAGAIN)
                           {
                                    sleep(1);
                                    write(STDOUT_FILENO, MSG_TRY,strlen(MSG_TRY));
                                    goto tryagain;
                            }
                            perror("read stdin");
                            exit(1);
                    }
                    write(STDOUT_FILENO, buf, n);
                    return 0;
             }

第二、select函数详细使用   

     select系统调用是用来让我们的程序监视多个文件句柄(file descriptor)的状态变化的。程序会停在select这里等待,直到被监视的文件句柄有某一个或多个发生了状态改变。

        文件在句柄在Linux里很多,如果你man某个函数,在函数返回值部分说到成功后有一个文件句柄被创建的都是的,如man socket可以看到“On success, a filedescriptor for the new socket is returned.”而man 2 open可以看到“open() and creat() return the new file descriptor”,其实文件句柄就是一个整数,看socket函数的声明就明白了:
int socket(int domain,int type, int protocol);
         当然,我们最熟悉的句柄是0、1、2三个,0是标准输入,1是标准输出,2是标准错误输出。0、1、2是整数表示的,对应的FILE *结构的表示就是stdin、stdout、stderr,0就是stdin,1就是stdout,2就是stderr。
比如下面这两段代码都是从标准输入读入9个字节字符:
#include<stdio.h>
#include <unistd.h>
#include <string.h>
int main(int argc, char ** argv)
{
        char buf[10] = "";
        read(0, buf, 9); /*
从标准输入 0 读入字符 */
        fprintf(stdout, "%s\n",buf); /*
向标准输出 stdout 写字符 */
        return 0;
}
/* **
上面和下面的代码都可以用来从标准输入读用户输入的9个字符** */
#include <stdio.h>
#include <unistd.h>
#include <string.h>
int main(int argc, char ** argv)
{
        char buf[10] = "";
        fread(buf, 9, 1, stdin); /*
从标准输入 stdin 读入字符 */
        write(1, buf, strlen(buf));
        return 0;
}

       继续上面说的select,就是用来监视某个或某些句柄的状态变化的。select函数原型如下:
int select(int nfds,fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
        函数的最后一个参数timeout显然是一个超时时间值,其类型是struct timeval *,即一个struct timeval结构的变量的指针,所以我们在程序里要申明一个struct timeval tv;然后把变量tv的地址&tv传递给select函数。struct timeval结构如下:

struct timeval {
             long    tv_sec;         /* seconds */
             long    tv_usec;        /* microseconds */
         };


第2、3、4三个参数是一样的类型: fd_set *,即我们在程序里要申明几个fd_set类型的变量,比如rdfds, wtfds, exfds,然后把这个变量的地址&rdfds, &wtfds,&exfds 传递给select函数。这三个参数都是一个句柄的集合,第一个rdfds是用来保存这样的句柄的:当句柄的状态变成可读的时系统就会告诉select函数返回,同理第二个wtfds是指有句柄状态变成可写的时系统就会告诉select函数返回,同理第三个参数exfds是特殊情况,即句柄上有特殊情况发生时系统会告诉select函数返回。特殊情况比如对方通过一个socket句柄发来了紧急数据。如果我们程序里只想检测某个socket是否有数据可读,我们可以这样:

fd_set rdfds; /* 先申明一个 fd_set 集合来保存我们要检测的 socket句柄 */
struct timeval tv; /*
申明一个时间变量来保存时间 */
int ret; /*
保存返回值 */
FD_ZERO(&rdfds); /*
select函数之前先把集合清零 */
FD_SET(socket, &rdfds); /*
把要检测的句柄socket加入到集合里 */
tv.tv_sec = 1;
tv.tv_usec = 500; /*
设置select等待的最大时间为1秒加500毫秒 */
ret = select(socket + 1, &rdfds, NULL, NULL, &tv); /*
检测我们上面设置到集合rdfds里的句柄是否有可读信息 */
if(ret < 0) perror("select");/*
这说明select函数出错 */
else if(ret == 0) printf("
超时\n"); /* 说明在我们设定的时间值1秒加500毫秒的时间内,socket的状态没有发生变化 */
else { /*
说明等待时间还未到1秒加500毫秒,socket的状态发生了变化 */
    printf("ret=%d\n", ret); /* ret
这个返回值记录了发生状态变化的句柄的数目,由于我们只监视了socket这一个句柄,所以这里一定ret=1,如果同时有多个句柄发生变化返回的就是句柄的总和了 */
    /*
这里我们就应该从socket这个句柄里读取数据了,因为select函数已经告诉我们这个句柄里有数据可读 */
    if(FD_ISSET(socket, &rdfds)) { /*
先判断一下socket这外被监视的句柄是否真的变成可读的了 */
        /*
读取socket句柄里的数据 */
        recv(...);
    }
}

注意select函数的第一个参数,是所有加入集合的句柄值的最大那个值还要加1。比如我们创建了3个句柄:
/************关于本文档********************************************
*filename: Linux网络编程一步一步学-select详解
*purpose: 详细说明select的用法
*********************************************************************/
int sa, sb, sc;
sa = socket(...); /*
分别创建3个句柄并连接到服务器上 */
connect(sa,...);
sb = socket(...);
connect(sb,...);
sc = socket(...);
connect(sc,...);

FD_SET(sa, &rdfds);/*
分别把3个句柄加入读监视集合里去 */
FD_SET(sb, &rdfds);
FD_SET(sc, &rdfds);

在使用select函数之前,一定要找到3个句柄中的最大值是哪个,我们一般定义一个变量来保存最大值,取得最大socket值如下:

int maxfd = 0;
if(sa > maxfd) maxfd = sa;
if(sb > maxfd) maxfd = sb;
if(sc > maxfd) maxfd = sc;

然后调用select函数:

ret = select(maxfd + 1,&rdfds, NULL, NULL, &tv); /* 注意是最大值还要加1 */

同样的道理,如果我们要检测用户是否按了键盘进行输入,我们就应该把标准输入0这个句柄放到select里来检测,如下:

FD_ZERO(&rdfds);
FD_SET(0, &rdfds);
tv.tv_sec = 1;
tv.tv_usec = 0;
ret = select(1, &rdfds, NULL, NULL, &tv); /*
注意是最大值还要加1 */
if(ret < 0) perror("select");/*
出错 */
else if(ret == 0) printf("
超时\n"); /* 在我们设定的时间tv内,用户没有按键盘 */
else { /*
用户有按键盘,要读取用户的输入 */
    scanf("%s", buf);
}

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值