UNIX环境高级编程(第14章 高级I/O)

来源:互联网 发布:java的静态代理 编辑:程序博客网 时间:2024/05/17 22:29

1非阻塞I/O

系统调用可分为低速系统调用和其他系统调用两类。低速系统调用是可能会使进程永远阻塞的一类系统调用,包括下列调用:

1)如果某些文件类型(如管道、终端设备和网络设备)的数据并不存在,则读操作可能会使调用者永远阻塞。

2)如果数据不能立即被上述同样类型的文件接受(由于在管道中无空间、网络流控制等)

3)在某种条件发生之前,打开某些类型的文件会被阻塞。

4)对已经加上强制性记录锁的文件进行读、写

5)某些ioctl操作

6)某些进程间通信函数

非阻塞I/O使得调用openreadwrite这样的I/O操作,并使这些操作不会永远阻塞。如果这种操作不能完成,则调用立即出错返回,表示该操作如继续执行将阻塞。

对于一个给定的描述符有两种方法对其指定非阻塞I/O:

1)如果调用open获得描述符,则可指定O_NONBLOCK标志。

2)对于已经打开的一个描述符,则可调用fcntl,由该函数打开O_NONBLOCK文件状态标志。

#include "apue.h"#include <fcntl.h>#include <errno.h>void set_fl(int fd, int flags){    int val;    if ((val = fcntl(fd, F_GETFL, 0)) < 0)    {        err_sys("fcntl F_GETFL error");    }    val |= flags; /* turn on flags */    if (fcntl(fd, F_SETFL, val) < 0)        err_sys("fcntl F_SETFL error");}void clr_fl(int fd, int flags){    int val;    if ((val = fcntl(fd, F_GETFL, 0)) < 0)    {        err_sys("fcntl F_GETFL error");    }    val &= ~flags; /* turn flags off*/    if (fcntl(fd, F_SETFL, val) < 0)        err_sys("fcntl F_SETFL error");}char buf[500000];int main(void){    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);    ptr = buf;    while (ntowrite > 0)    {        errno = 0;        nwrite = write(STDOUT_FILENO, ptr, ntowrite);        fprintf(stderr, "nwrite = %d, errno = %d\n", nwrite, errno);        if (nwrite > 0)        {            ptr += nwrite;            ntowrite -= nwrite;        }    }        clr_fl(STDOUT_FILENO, O_NONBLOCK);    exit(0);}

[root]# ./a.out < /etc/termcap > temp.file        /*标准输出是普通文件,则可以期望write只执行一次*/read 500000 bytesnwrite = 500000, errno = 0[root]# ls -l temp.file-rwxrwSrwx 1 root root 500000 2015-08-27 temp.file[root]# [root]# ./a.out < /etc/termcap 2>stderr.out      /*若标准输出是终端,则期望write有时会返回小于500000的一个数字*/[root]# cat stderr.out  | more                   /*有时则出错返回*/read 500000 bytesnwrite = 8113, errno = 0nwrite = -1, errno = 11nwrite = -1, errno = 11nwrite = -1, errno = 11......nwrite = -1, errno = 11nwrite = -1, errno = 11nwrite = -1, errno = 11nwrite = 4011, errno = 0nwrite = -1, errno = 11nwrite = -1, errno = 11......

2文件锁与记录锁

1)文件锁

文件锁的功能是对整个文件加锁,不能锁定文件的某一个区域。

文件锁是通过flock函数来实现的:

头文件

#include <sys/file.h>

函数原型

int flock(int fd, int operation);

参数

fd:文件描述符

operation:有下列四种情况:

LOCK_SH建立共享锁定。多个进程可同时对同一个文件作共享锁定。

  LOCK_EX建立互斥锁定。一个文件同时只有一个互斥锁定。

  LOCK_UN解除文件锁定状态。

  LOCK_NB无法建立锁定时,此操作可不被阻断,马上返回进程。通常与LOCK_SHLOCK_EXOR(|)组合。

 

返回

成功返回0,失败返回-1并记录errno

功能

锁定整个文件


/*文件锁用例:父、子进程向同一个文件test分别写a和b*/#include <stdio.h>#include <stdlib.h>#include <sys/file.h>#include <sys/wait.h>int main(void){    pid_t pid;    int fd;    int i = 0;    int ret = 0;        pid = fork();    if (pid < 0)    {        printf("fork failed..\n");        return -1;    }    else if (pid == 0)    {        int i = 0;        int ret = 0;        printf("child start ...\n");                fd = open("/home/test", O_RDWR | O_APPEND | O_CREAT);        if (fd == -1)        {            printf("open failed..\n");            return -1;        }        /*子进程尝试加文件锁*/        ret = flock(fd, LOCK_EX);        if (ret == -1)        {            printf("flock failed..\n");            return -1;        }                write(fd, "\nchild text start\n", 19);        while (i++ < 5000)        {            write(fd, "a", 2);        }        write(fd, "\nchild text end\n", 17);                /*子进程解锁*/        ret = flock(fd, LOCK_UN);        if (ret == -1)        {            printf("flock failed..\n");            return -1;        }        printf("child end...\n");        exit(0);    }    printf("parent start ...\n");    fd = open("/home/test", O_RDWR | O_APPEND | O_CREAT);    if (fd == -1)    {        printf("open failed..\n");        return -1;    }    /*父进程加文件锁*/    ret = flock(fd, LOCK_EX);    if (ret == -1)    {        printf("flock failed..\n");        return -1;    }    write(fd, "\nparent text start\n", 20);    while (i++ < 5000)    {        write(fd, "b", 2);    }    write(fd, "\nparent text end\n", 18);        /*父进程解锁*/    ret = flock(fd, LOCK_UN);    if (ret == -1)    {        printf("flock failed..\n");        return -1;    }    printf("\nparent end ...\n");    return 0;}

flock文件锁对同一个文件进行加锁,以防止不同的进程发生竞争。但如果有一个进程A不加文件锁而去进行写操作时,即使已经有进程B持有文件锁了,进程A仍然可以写数据到文件。为了真正防止竞争,所有对该文件进行操作的进程都必须加文件锁。


2)记录锁

记录锁(recordlocking)的功能是:当一个进程正在读或修改文件的某个部分时,它可以阻止其他进程修改同一文件区。“记录”这个词是一种误用,更合适的术语可能是字节返回锁(byte-range locking),因为它锁定的只是文件中的一个区域(也可能是整个文件)。

记录锁是通过fcntl函数来实现的:

头文件

#include <fcntl.h>

函数原型

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

参数

filedes:文件描述符

cmd:对于记录锁,cmdF_GETLKF_SETLKF_SETLKW

flockptr:flcok结构指针

struct flock{

short l_type;/*锁类型:F_RDLCK(共享读锁)F_WRLCK(独占性写锁)

F_UNLCK(解锁一个区域)*/

off_t l_start;/*相对偏移量(字节)*/

short l_whence;/*相对偏移量的起点,SEEK_SETSEEK_CURSEEK_END*/

off_t l_len;/*加锁或解锁区域的字节长度*/

pid_t l_pid;/*具有能阻塞当前进程的锁,其持有进程的ID存放在l_pid*/

};

返回

若成功则依赖cmd,若出错则返回-1

功能

实现记录锁,以锁定整个文件或文件的一个区域。

为了锁定整个文件:设置偏移量起点l_whenceSEEK_SET,偏移量l_start0,区域长度l_len0

#include <unistd.h>#include <fcntl.h>/*利用fcntl实现锁定整个文件*/int lockfile(int fd){    struct flock fl;    fl.l_type = F_WRLCK;    fl.l_start = 0;    fl.l_whence = SEEK_SET;    fl.l_len = 0;    return (fcntl(fd, F_SETLK, &fl));}

3 I/O多路转接

问题:

telnet进程为例,该程序读终端(标准输入),将所得数据写到网络连接;同时读网络连接,将所得数据写到终端上(标准输出)。

telnet进程有两个输入、两个输出。对这两个输入中的任何一个都不能使用阻塞read,因为我们永远不知道哪一个输入有我们需要的数据。

解决方法:

1)用fork将一个进程变成两个进程,每个进程处理一条数据通路。(或使用一个进程中的两个线程)

弊端:进程终止处理比较复杂或线程之间同步的处理比较复杂。

2)利用轮询方法。即使用一个进程执行该程序,但使用非阻塞I/O读数据。简单说就是无数据可读时返回,等若干秒在读。

弊端:浪费CPU时间。

3)异步I/O。利用信号通知。

弊端:信号使用受限。

4I/O多路转接。先构造一张有关描述符的列表,然后调用一个函数,直到这些描述符中的一个已准备好进行I/O时,该函数才返回。在返回时,它告诉进程哪些描述符已准备好可以进行I/O

   利用pollpselectselect这三个函数执行I/O多路转接:

头文件

#include <sys/select.h>

函数原型

int select(int maxfdp1, fd_set *resctrict readfds, fd_set *restrict writefds,

 fd_set *restrict exceptfds, struct timeval *restrict tvptr);

参数

maxfdp1:最大描述符加1。在三个描述符集中找到最大描述符编号值,加1

readfds:可读描述符集

writefds:可写描述符集

exceptfds:异常描述符集

tvptr:愿意等待的时间

返回

返回准备就绪的描述符数,若超时则返回0,出错返回-1

功能

实现I/O多路转接

   传向select的参数告诉内核:

(1)我们所关心的描述符

(2)对于每个描述符我们所关心的状态

(3)愿意等多长时间

(4)已准备好的描述符的数量

(5)对于读、写或异常这三个状态中的每一个,哪些描述符已准备好


头文件

#include <poll.h>

函数原型

int poll(struct pollfd fdarray[], nfds_t nfds, int timeout);

参数

fdarray:

struct pollfd{

   int fd;/*描述符*/

   short events;/*对描述符关心的是什么*/

   short revents;/*该描述符已经发生了什么事件*/

}

nfds:fdarray数组元素的个数

timeout:愿意等多少时间,单位毫秒

timeout==-1 永远等待

timeout==0  不等待

timeout>0   等待timeout毫秒

返回

返回准备就绪的描述符数,若超时则返回0,出错返回-1

功能

实现I/O多路转接。Poll函数类似于select,但是其程序员接口有所不同

poll函数用于监测多个等待事件,若事件未发生,进程睡眠,放弃CPU控制权,若监测的任何一个事件发生,poll将唤醒睡眠的进程,并判断是什么等待事件发生,执行相应的操作。poll函数退出后,struct pollfd变量的所有值被清零,需要重新设置。

pollfd中的events成员应该设置为下表的标志名,poll用已经发生的事件填充revents

标志名

标志值

说明

POLLIN

POLLRDNORM

POLLRDBAND

POLLPRI

0x0001

0x0040

0x0080

0x0002

不阻塞地可读除高优先级外的数据

不阻塞地可读普通数据

不阻塞地可读非0优先级波段数据

不阻塞地可读高优先级数据

POLLOUT

POLLWRNORM

POLLWRBAND

0x0004

0x0100

0x0200

不阻塞地可写普通数据

POLLOUT相同

不阻塞地可写非0优先级波段数据

POLLERR

POLLHUP

POLLNVAL

0x0008

0x0010

0x0020

已出错

已挂断

描述符不引用一打开文件

struct pollfd poll_array;poll_array.fd = fd;         /*文件描述符*/poll_array.events = POLLIN; /*等待的事件*/for (;;){    ret = poll(&poll_array, 1, timeout);    if (ret > 0)    {        if (poll_array.revents)        {            /*do something*/        }    }    else if (ret < 0)    {        if (EINTR == errno) {}        else {/*do something*/ break;}            }}


4存储映射I/O

存储映射I/O使一个磁盘文件与存储空间中的一个缓冲区相映射,于是当从缓冲区中取出数据,就相当于读文件中的相应字节。与此类似,将数据存入缓冲区,则相应字节就自动地写入文件。这样就可以在不使用readwrite的情况下执行I/O

为使用这种功能,应先告诉内核将一个给定的文件映射到一个存储区域中。该功能由mmap函数实现。

#include <sys/mman.h>

void *mmap(void *addr, size_t len, int prot, int flag, int fiedes, off_t off);

返回值:若成功则返回映射区的起始地址,若出错则返回MAP_FAILED

参数:

addr:用于指定映射存储区的起始地址。通常将其设置为0,表示由系统选择该映射区的起始地址。(映射存储区位于堆和栈之间)

len:映射的字节数

prot:说明对映射存储区的保护要求。不能超过文件open模式访问权限。

PROT_READ 映射区可读

PROT_WRITE映射区可写

PROT_EXEC 映射区可执行

PROT_NONE映射区不可访问

flag:影响映射存储区的多种属性

MAP_SHARED该标志指定存储操作(相当于对该文件的write)修改映射文件

MAP_PRIVATE该标志说明对映射区的存储操作导致创建该映射文件的一个私有副本。

fiedes:指定要被映射文件的描述符。在映射该文件到一个地址空间之前,先要打开该文件。

off:要映射字节在文件中的起始偏移量。通常指定为0.

与映射存储区相关的有SIGSEGVSIGBUS两个信号。

信号SIGSEGV通常用于指示进程试图访问对它不可用的存储区。

如果访问映射区的某个部分,而在访问时这一部分实际上已不存在,则产生SIGBUS信号。

/*用存储映射I/O复制一个文件,类似cp命令*/#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <fcntl.h>#include <signal.h>#include <string.h>#include <sys/mman.h>#include <sys/stat.h>static void sig_bus(int signo){    printf("caught SIGBUS\n");    return ;}int main(int argc, char *argv[]){    int fdin, fdout;    void *src, *dst;    struct stat statbuf;    if (argc != 3)    {        printf("usage: %s <fromfile> <tofile>\n", argv[0]);        return -1;    }    if (signal(SIGBUS, sig_bus) == SIG_ERR)        printf("signal error");    if ((fdin = open(argv[1], O_RDONLY)) < 0)    {        printf("can't open %s for reading\n", argv[1]);    }    if ((fdout = open(argv[2], O_RDWR|O_CREAT|O_TRUNC, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) < 0)    {        printf("can't create %s for writing\n", argv[2]);    }    /*获取输入文件的长度*/    if (fstat(fdin, &statbuf) < 0)    {        printf("fstat error");    }    /*设置输出文件的长度*/    #if 1 /*若不设置文件长度则会产生SIGBUS信号*/    if (lseek(fdout, statbuf.st_size - 1, SEEK_SET) == -1)        printf("lseek error");    if (write(fdout, "", 1) != 1)        printf("write error");    #endif        if ((src = mmap(0, statbuf.st_size, PROT_READ, MAP_SHARED, fdin, 0)) == MAP_FAILED)        printf("mmap error for input");    if ((dst = mmap(0, statbuf.st_size, PROT_READ | PROT_WRITE, MAP_SHARED, fdout, 0)) == MAP_FAILED)        printf("mmap error for output");    memcpy(dst, src, statbuf.st_size);    exit(0);}

[root]# ./a.out 14recordlock.c dst  /*没有设置输出文件长度的情况*/caught SIGBUS总线错误(coredump)

调用mprotect可以更改一个现存映射存储区的权限。

#include <sys/mman.h>

int mprotect(void *addr, size_t len, int prot);

返回值:若成功则返回0,若出错则返回-1

参数:

addr:存储区地址

len:字节长度

prot:存储区权限,与mmap中的prot一样。

    调用msync可以将映射存储区中已被修改的页冲洗到文件中。

#include <sys/mman.h>

int msync(void *addr, size_t len, int flags);

返回值:若成功则返回0,若出错则返回-1

参数:

addr:存储映射区地址,地址必须与页界面对齐

len:字节数

flagsMS_SYNC标志或MS_ASYNC标志中的一个

    进程终止时,或调用munmap之后,存储映射区就被自动解除映射。关闭文件描述符filedes并不解除映射区。

#include <sys/mman.h>

int munmap(caddr_t addr, size_t len);

返回值:若成功则返回0,若出错则返回-1

   munmap不会影响被映射的对象,即调用munmap不会使映射区的内容写到磁盘文件上。


 









0 0
原创粉丝点击