我是靠谱客的博主 甜蜜楼房,最近开发中收集的这篇文章主要介绍LINUX系统编程--7 高级IO七 高级IO,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

LINUX系统编程--7 高级IO

  • 七 高级IO
    • 1 非阻塞IO与阻塞
    • 2 数据中继
    • 3 有限状态机
    • 4 用有限状态机实现一个数据中继模型(使用非阻塞IO)
    • 4.1 中继引擎的实现
    • 5 IO 多路转接
    • 6 IO 多路转接-select
    • 7 IO 多路转接-poll
    • 8 IO 多路转接-epoll
    • 9 其他读写相关函数
    • 10 存储映射
    • 11 文件锁

七 高级IO

高级IO主要研究非阻塞IO

首先要区分阻塞IO与非阻塞IO

阻塞和非阻塞是文件本身的属性

回忆在信号中:
信号会打断阻塞中的系统调用。(如open这个系统调用,有可能被信号打断,打断会返回EINTR,这个是假错! open没有出错,只是在阻塞的过程中被信号处理函数打断了!)

还有一种假错,叫EAGAIN,这也是一个假错!他的场景是IO是非阻塞的。以read为例,当read是以非阻塞形式读的时候后,读不到东西会马上返回,并设置error为EAGAIN,这也是一种假错,并不是read出错了,而是没有读到东西。

阻塞和非阻塞是文件本身的属性,而不是read的属性!

内容:
1、有限状态机编程、
2、非阻塞IO
3、IO多路转接
4、其他读写函数
5、存储映射IO
6、文件锁

1 非阻塞IO与阻塞

之前所做IO的都是阻塞的。
假设有两个设备,在进行通信。
假设要进行读左写右

有限状态机可以解决复杂流程的问题。

简单流程:一个程序的自然流程是结构化的
复杂流程:一个程序的自然流程不是结构化的。如网络协议,网络协议的处理是很复杂的

(自然流程:如大象装冰箱)

2 数据中继

两个设备之间的交换数据:
在这里插入图片描述
若使一个进程完成这个流程:读左-写右-读右-写左
如果是以阻塞IO的形式进行,那么若1号设备一直没有信息进来,那么会使读左的操作一直阻塞。
解决的方法有两个:

  • 设计两个任务/进程/线程,一个任务进行读左写右,另一个任务进行读右写左
  • 将阻塞IO换成非阻塞IO

3 有限状态机

在这里插入图片描述
在这里插入图片描述

通过Executable Code实现映射的FSM:

  • 这种方式主要是通过条件分支来处理不同的字符,如if或者switch语句块(下面实例使用这个方法)

通过Passive Data实现映射的FSM:

  • 在如上的switch分支中,其使用类型大致相同,因此,我们可以考虑将相似的信息保存到一张表中,这样就可以在程序中避免很多函数调用。在每个状态中都使用一张转换表来表示映射关系,转换表的索引使用输入字符来表示。此外,由于通过转换表就可以描述不同状态之间的变化,那么就没有必要将每种状态定义为一个类了,即不需要多余的继承和虚函数了,仅使用一个State即可。

相关链接: 有限状态机的原理解析与编程思路.

4 用有限状态机实现一个数据中继模型(使用非阻塞IO)

首先画出状态转换图:
在这里插入图片描述

//relay.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <signal.h>
#include <errno.h>

#define BUFSIZE 1024
#define TTY1 "/dev/tty11"
#define TTY2 "/dev/tty12"

enum {
    STATE_R = 1,
    STATE_W,
    STATE_Ex,
    STATE_T
};

struct fsm_st{  //这就是状态机结构!
    int state;
    int sfd;//源
    int dfd;//目标
    char buf[BUFSIZE];
    int len;
    int pos;
    //出错原因
    char *errMess;
};

static void fsm_driver(struct fsm_st* fsm){//推状态机函数
    int ret;

    switch(fsm->state){
        case STATE_R:
            fsm->len = read(fsm->sfd,fsm->buf,BUFSIZE);
            if (fsm->len == 0){
                fsm->state = STATE_T;//正常结束
            }else if (fsm->len < 0){
                if (errno == EAGAIN)
                  fsm->state = STATE_R;//假错 保留状态
                else{
                    fsm->state = STATE_Ex;//真正出错
                    fsm->errMess = "读取失败";
                }

            }else{
                fsm->pos = 0;
                fsm->state = STATE_W;//成功读入 转换状态
            }

            break;
        case STATE_W:
            ret = write(fsm->dfd,fsm->buf+fsm->pos,fsm->len);
            if (ret < 0){
                if (errno == EAGAIN){
                    fsm->state = STATE_W;
                }else{
                    fsm->errMess = "写入失败";
                    fsm->state = STATE_Ex;
                }
            }else{
                //坚持写够
                fsm->pos += ret;
                fsm->len -= ret;
                if (fsm->len == 0){
                    fsm->state = STATE_R;
                }else{
                    fsm->state = STATE_W;
                }
            }

            break;
        case STATE_Ex:
            perror(fsm->errMess);
            fsm->state = STATE_T;
            break;
        case STATE_T:
            /*do sth*/
            break;
        default:
            abort();
            break;
    }

}

static void relay(int fd1,int fd2){  //中继引擎函数
    int old_fd1,old_fd2;

    struct fsm_st fsm12,fsm21;//定义状态机。读左写右 读右写左

    old_fd1 = fcntl(fd1,F_GETFL);
    fcntl(fd1,F_SETFL,old_fd1|O_NONBLOCK);//设为非阻塞

    old_fd2 = fcntl(fd2,F_GETFL);
    fcntl(fd2,F_SETFL,old_fd2|O_NONBLOCK);//设为非阻塞

    fsm12.state = STATE_R;
    fsm12.sfd = fd1;
    fsm12.dfd = fd2;

    fsm21.state = STATE_R;
    fsm21.sfd = fd2;
    fsm21.dfd = fd1;

    while(fsm12.state != STATE_T || fsm21.state != STATE_T){//状态机没有停转
        fsm_driver(&fsm12);//推状态机
        fsm_driver(&fsm21);//推状态机

    }
    

    //恢复原来的文件描述符状态
    fcntl(fd1,F_SETFL,old_fd1);
    fcntl(fd2,F_SETFL,old_fd2);

}

int main()//main函数通常是模拟用户的操作
{
    int fd1,fd2;
    //假设用户使用阻塞的方式打开设备
    fd1 = open(TTY1,O_RDWR);
    if (fd1 < 0){
        perror("open()");
        exit(1);
    }
    write(fd1,"TTY1n",5);

    fd2 = open(TTY2,O_RDWR|O_NONBLOCK);
    if (fd2 < 0){
        perror("open()");
        close(fd1);
        exit(1);
    }
    write(fd2,"TTY2n",5);

    relay(fd1,fd2);//中继引擎函数

    close(fd1);
    close(fd2);
    exit(0);
}

4.1 中继引擎的实现

5 IO 多路转接

IO多路转接,监视文件描述符读,写,异常状态

上面的relay.c,处于盲等状态,原因在于一直在处于EAGAIN,若没有 数据,一直会盲等。

为什么引入多路转接:

  • 在前面 状态机实现拷贝两个设备之间的数据的程序中,可以发现这样的问题:程序在运行期间,一定是一个忙等的状态,CPU 使用率很高,很浪费CPU时间,原因在于程序其实大部分时间都在忙于 判断假错,重读 重写的状态中循环。这个属于IO 密集,负载不密集的任务,即数据量不大,但是IO 很密集。对于IO密集型的任务 可以对程序进行IO多路转接,本质就是监视文件描述符的行为,当 当前文件描述符发生了我感兴趣的行为的时候,才会去做后续操作,如前面 两个设备之间的数据交换,前面的做法是盲推,一直不停的探测,尝试,看有没有内容可读或者可写。如果我们用IO 多路转接,就可以变成 当某个文件描述符状态发生了我感兴趣的动作的时候,我才会做后续操作,节省CPU时间。

  • 解决IO密集型任务中盲等的问题,监视文件描述符的行为,当当前文件描述符发生了我们感兴趣的行为时,才去做后续操作,可以实现安全的休眠(替代sleep)

IO多路转接比较:

  • select() :比较古老,可移植,兼容性好 但设计有缺陷,以事件为单位组织文件描述符。
  • poll():可移植,以文件描述符为单位组织事件。
  • epoll ():linux的poll封装方案,不可移植

6 IO 多路转接-select

可以实现安全的休眠(替代sleep)

在这里插入图片描述

int select(int nfds, fd_set *readfds, fd_set *writefds,
              fd_set *exceptfds, struct timeval *timeout);
/*  nfds : 文件描述符数量,不过指的是,所包含的最大文件描述符 +1 的值
  readfds:可读的文件描述符集合
  writefds:可写的文件描述符集合
  exceptfds:异常的文件描述符集合
  timeout 超时设置,阻塞。如果不实现超时设置,那么该函数会死等
 */
void FD_CLR(int fd, fd_set *set);//从指定的文件描述符集合中删除指定文件描述符fd
int  FD_ISSET(int fd, fd_set *set);//判断文件描述符fd 是否存在于文件描述符集合set中
void FD_SET(int fd, fd_set *set);// 将文件描述符fd 放到 文件描述符集合set中
void FD_ZERO(fd_set *set); //清空一个文件描述符集合

selectd的返回值:

  • 返回值是现在发生了我们感兴趣事件的文件描述符行为的个数,并且依然将这些文件描述符放在 读集,写集,错误集这些文件描述符集合中。
  • 失败返回-1,设置errno。EINTR 属于假错,阻塞等待可以被信号打断
ERROR:
   EINTR  A signal was caught; see signal(7). 阻塞等待 可以被信号打断

   EINVAL nfds is negative or exceeds the RLIMIT_NOFILE resource limit (see getrlimit(2)).

   EINVAL the value contained within timeout is invalid.

   ENOMEM unable to allocate memory for internal tables.


The time structures involved are defined in <sys/time.h> and look like

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

实验:改进上面的用状态机实现的中继模型,加入多路IO转接机制(select)
(注意:只对relay函数做出了改变)
程序就不再是一直处于 假错 重读 重写 中循环,cpu使用率大大降低,因为大多数时间都阻塞在 select()等待文件描述符变化。

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/select.h>

#define TTY1 "/dev/tty11"
#define TTY2 "/dev/tty12"
#define BUFSIZE 1024

enum
{
	STATE_R = 1,
	STATE_W,
STATE_AUTO,
	STATE_Ex,
	STATE_T
};

struct fsm_st
{
	int state;
	int sfd;
	int dfd;
	char buf[BUFSIZE];
	int len;
	int pos;
	char* errstr;
};

static void fsm_driver(struct fsm_st *fsm)
{
	int ret;

	switch(fsm->state)
	{
		case STATE_R:
			fsm->len = read(fsm->sfd,fsm->buf,BUFSIZE);
			if(fsm->len == 0)
				fsm->state = STATE_T;
			else if(fsm->len < 0)
			{
				if(errno == EAGAIN)
					fsm->state = STATE_R;

				else
				{
					fsm->errstr = "read()";
					fsm->state = STATE_Ex;
				}	
					
			}
			else
			{		
				fsm->pos = 0;
				fsm->state = STATE_W;
			}
		
		break;

		case STATE_W:
			ret = write(fsm->dfd,fsm->buf+fsm->pos,fsm->len);
			if(ret < 0)
			{
				if(errno == EAGAIN)
					fsm->state = STATE_W;
				else
				{
					fsm->errstr = "read()";
					fsm->state = STATE_Ex;
				}
				
			}
			else
			{
				fsm->pos += ret;
				fsm->len -= ret;
				if(fsm->len == 0)
					fsm->state = STATE_R;
				else 
					fsm->state = STATE_W;
			}
		break;

		case STATE_Ex:
			perror(fsm->errstr);
			fsm->state = STATE_T;		
		break;

		case STATE_T:
			/* do something*/
		break;
		
		default:
			abort();
		break;
	}

}

static int max(int a,int b)
{
	if(a > b)
	{	
		return a;
	}
	return b;
}

static void relay(int fd1,int fd2)
{
	int fd1_save,fd2_save;
	struct fsm_st fsm12,fsm21;
	fd_set rset,wset;	


	fd1_save = fcntl(fd1,F_GETFL);
	fcntl(fd1,F_SETFL,fd1_save|O_NONBLOCK);

	fd2_save = fcntl(fd2,F_GETFL);
	fcntl(fd2,F_SETFL,fd2_save|O_NONBLOCK);

	fsm12.state = STATE_R;
	fsm12.sfd = fd1;
	fsm12.dfd = fd2;

	fsm21.state = STATE_W;
	fsm21.sfd = fd2;
	fsm21.dfd = fd1;	

	while(fsm12.state != STATE_T || fsm21.state != STATE_T)	//状态机没有停转
	{
	//设置现场
		FD_ZERO(&rset);
		FD_ZERO(&wset);

		if(fsm12.state == STATE_R)
			FD_SET(fsm12.sfd,&rset);

		if(fsm12.state == STATE_W)
			FD_SET(fsm12.dfd,&wset);

		if(fsm21.state == STATE_R)
			FD_SET(fsm21.sfd,&rset);

		if(fsm21.state == STATE_W)
			FD_SET(fsm21.dfd,&wset);

  	//监控
		if(fsm12.state < STATE_AUTO || fsm21.state < STATE_AUTO)
		{
			if(select(max(fd1,fd2)+1,&rset,&wset,NULL,NULL) < 0)	
			{
				if(errno == EINTR)
					continue;

				perror("select()");
				exit(1);
			}
		}
/*
	struct  timeval ts;
        ts.tv_sec = 0;
        ts.tv_usec= 2;
        int maxfd = fd1>fd2?fd1:fd2;
        if (fsm12.state < STATE_AUTO ||fsm21.state < STATE_AUTO)
        {
            if (select(maxfd+1,&rset,&wset,NULL,&ts) < 0)
            {
                if (errno == EINTR)
                  continue;
                perror("select()");
                exit(1);
            }
        }
*/
	
 	//根据监控结果 最下一步动作
		if(FD_ISSET(fd1,&rset) || FD_ISSET(fd2,&wset) || fsm12.state > STATE_AUTO)
		fsm_driver(&fsm12);

		if(FD_ISSET(fd2,&rset) || FD_ISSET(fd1,&wset) || fsm12.state > STATE_AUTO)
		fsm_driver(&fsm21);
	}

	fcntl(fd1,F_SETFL,fd1_save);
	fcntl(fd2,F_SETFL,fd2_save);

}

int main()
{

	int fd1,fd2;

	fd1 = open(TTY1,O_RDWR);
	if(fd1 < 0)
	{

		perror("open()");
		exit(1);
	}

	fd2 = open(TTY2,O_RDWR|O_NONBLOCK);
	if(fd1 < 0)
	{
		perror("open()");
		exit(1);
	}

	relay(fd1,fd2);


	close(fd2);
	close(fd1);
	

}

注意:

  • 该函数 如果不实现超时设置,那么该函数会阻塞死等,一直等到感兴趣的事件发生,即关心的可读,可写的文件描述符集合中 文件描述符变成 可读,可写状态的时候,该函数才会返回。
  • 注意三个 fd_set 不是const类型,是可变的。当select()返回的时候,这三个集合当中就不再是我们之前所布置的监事现场了,而是存放结果的场所。如果select()不设置超时时间 阻塞等待 被信号打断,发生了假错,那么这三个集合也将被清空。

select()的问题:

  • 1 监视现场位置(三个集合) 和 监视结果位置(三个集合) 相同,用的是同一块空间,如果 读集合,写集合,出错集合 各设置10个文件描述符,一共三十个文件描述符,只要任意一个文件描述符发生变化,如读集中某一个文件描述符 变成了可读状态,则函数返回,即返回读集合中能读的文件描述符个数,即1。而其他集合就会被清空。再次监控,需要重新设置监视现场。

  • 2 一个进程中能够打开的文件描述符地方个数是可以更改的,ulimit -a。 那么第一个参数 nfds就有问题,如果我们监视的文件描述符个数 超过了 有符号整形最大值范围,则会溢出。

7 IO 多路转接-poll

poll 引进: 补充 select()的不足。他俩的区别在于:

  • 1 poll() 的 监视现场位置 和 监视结果位置分开存放,不需要向select()一样 每次返回都发生覆盖,不需要重复设置监视现场
  • 2 poll() 所能监控的状态有7中,并且可以自己添加需要的其他状态位,比select()多
 int poll(struct pollfd *fds, nfds_t nfds, int timeout);
 
功能与select()相似,等待目标文件描述符发生所需要的的变化,返回。

参数:
fds: pollfd 结构体数组的起始位置。
nfds:数组元素个数,即有多少个需要监视的文件描述符
timeout: 超时设置,单位是毫秒,即设置1000,为1秒。如果不设置超时,则函数阻塞等待
	- 0: 以非阻塞方式 poll()
	- -1: 阻塞方式poll()
	- 时间:超时设置


返回值:
成功返回一个整数,表示有多少个事件已经发生,失败返回-1,并且设置 errno,如假错EINTR  

ERRORS
       EFAULT The array given as argument was not contained in the calling program's address space.

       EINTR  A signal occurred before any requested event; see signal(7). 假错,信号打断阻塞 poll

       EINVAL The nfds value exceeds the RLIMIT_NOFILE value.

       ENOMEM There was no space to allocate file descriptor tables.
pollfd中记录一个文件描述符相关内容,包括对文件描述符所关心的状态,和返回的状态。
events 和 revents是两个位图,有7中状态,比select多,select只有3种状态,除了读写 都是异常
events 和 revents是两个位图 可以添加我们需要的状态
           
struct pollfd 
{
    int   fd;         /* file descriptor */
    short events;     /* requested events */ 所关心的状态
    short revents;    /* returned events */已经发生的事件,与所关心的事件分开存放!与select()不同
};

   The bits that may be set/returned in events and revents are defined in <poll.h>:

          POLLIN 是否可读

          POLLPRI
                 There is urgent data to read (e.g., out-of-band data on TCP socket; pseudoterminal master in packet mode has seen state change in slave).

          POLLOUT 是否可写
                 

          POLLRDHUP (since Linux 2.6.17)
                 Stream  socket  peer closed connection, or shut down writing half of connection.  The _GNU_SOURCE feature test macro must be defined (before including any header files) in order
                 to obtain this definition.

          POLLERR
                 Error condition (only returned in revents; ignored in events).

          POLLHUP
                 Hang up (only returned in revents; ignored in events).  Note that when reading from a channel such as a pipe or a stream socket, this event merely indicates that the peer closed
                 its end of the channel.  Subsequent reads from the channel will return 0 (end of file) only after all outstanding data in the channel has been consumed.

          POLLNVAL
                 Invalid request: fd not open (only returned in revents; ignored in events).

实验:改进上面的用状态机实现的中继模型,加入多路IO转接机制(poll)

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
#include <fcntl.h>
//#include <sys/select.h>
#include <poll.h>

#define TTY1 "/dev/tty11"
#define TTY2 "/dev/tty12"
#define BUFSIZE 1024

enum
{
	STATE_R = 1,
	STATE_W,
STATE_AUTO,
	STATE_Ex,
	STATE_T
};

struct fsm_st
{
	int state;
	int sfd;
	int dfd;
	char buf[BUFSIZE];
	int len;
	int pos;
	char* errstr;
};

static void fsm_driver(struct fsm_st *fsm)
{
	int ret;

	switch(fsm->state)
	{
		case STATE_R:
			fsm->len = read(fsm->sfd,fsm->buf,BUFSIZE);
			if(fsm->len == 0)
				fsm->state = STATE_T;
			else if(fsm->len < 0)
			{
				if(errno == EAGAIN)
					fsm->state = STATE_R;

				else
				{
					fsm->errstr = "read()";
					fsm->state = STATE_Ex;
				}	
					
			}
			else
			{		
				fsm->pos = 0;
				fsm->state = STATE_W;
			}
		
		break;

		case STATE_W:
			ret = write(fsm->dfd,fsm->buf+fsm->pos,fsm->len);
			if(ret < 0)
			{
				if(errno == EAGAIN)
					fsm->state = STATE_W;
				else
				{
					fsm->errstr = "read()";
					fsm->state = STATE_Ex;
				}
				
			}
			else
			{
				fsm->pos += ret;
				fsm->len -= ret;
				if(fsm->len == 0)
					fsm->state = STATE_R;
				else 
					fsm->state = STATE_W;
			}
		break;

		case STATE_Ex:
			perror(fsm->errstr);
			fsm->state = STATE_T;		
		break;

		case STATE_T:
			/* do something*/
		break;
		
		default:
			abort();
		break;
	}

}

static void relay(int fd1,int fd2)
{
	int fd1_save,fd2_save;
	struct fsm_st fsm12,fsm21;
	struct pollfd pfd[2];	


	fd1_save = fcntl(fd1,F_GETFL);
	fcntl(fd1,F_SETFL,fd1_save|O_NONBLOCK);

	fd2_save = fcntl(fd2,F_GETFL);
	fcntl(fd2,F_SETFL,fd2_save|O_NONBLOCK);

	fsm12.state = STATE_R;

	fsm12.sfd = fd1;
	fsm12.dfd = fd2;

	fsm21.state = STATE_W;
	fsm21.sfd = fd2;
	fsm21.dfd = fd1;	

//布置 监视现场
	pfd[0].fd = fd1;
	pfd[1].fd = fd1;

	while(fsm12.state != STATE_T || fsm21.state != STATE_T)	
	{
		//以文件描述符为单位组织事件,设置文件描述符所关心的状态,如状态机fsm12 为读状态时候,关心的文件描述符状态为读,只要文件可读,就 通知poll()返回,即只要目标文件中有内容可读,这里就是当TTY11 中有户数输入后,即为可读。
		pfd[0].events = 0;
		if(fsm12.state == STATE_R)
			pfd[0].events |= POLLIN;
		if(fsm21.state == STATE_W)
			pfd[0].events |= POLLOUT;

		pfd[1].events = 0;
		if(fsm12.state == STATE_W)
			pfd[1].events |= POLLOUT;

		if(fsm21.state == STATE_R)
			pfd[1].events |= POLLIN;
		
		if(fsm12.state < STATE_AUTO || fsm21.state < STATE_AUTO)
		{
//等待目标文件描述符发生状态变化,这里就是当TTY11 中有户数输入后,即为可读,则通知poll返回。
			while(poll(pfd,2,-1) < 0)
			{
				if(errno == EINTR)
					continue;

				perror("poll()");
				exit(1);
			}
		}
	

		if(pfd[0].revents & POLLIN || pfd[1].revents & POLLOUT || fsm12.state > STATE_AUTO)
		fsm_driver(&fsm12);

		if(pfd[1].revents & POLLIN || pfd[0].revents & POLLOUT || fsm12.state > STATE_AUTO)
		fsm_driver(&fsm21);
	}

	fcntl(fd1,F_SETFL,fd1_save);
	fcntl(fd2,F_SETFL,fd2_save);

}

int main()
{

	int fd1,fd2;

	fd1 = open(TTY1,O_RDWR);
	if(fd1 < 0)
	{

		perror("open()");
		exit(1);
	}

	fd2 = open(TTY2,O_RDWR|O_NONBLOCK);
	if(fd1 < 0)
	{
		perror("open()");
		exit(1);
	}

	relay(fd1,fd2);


	close(fd2);
	close(fd1);
	

}

8 IO 多路转接-epoll

epoll不可移植。

epoll VS poll

  • poll() 可以理解为我们在用户态创建并维护一个结构体数组,而 epoll() 则是相当于,poll 中的结构体数组被放在内核态,内核为我们维护该数组,内核为我们提供一些方法(系统调用,即下面的三个函数)来管理这个数组。
  • epoll是针对linux优化了的,性能要比poll好。

需要三部分:
1、int epoll_create(int size);

  • 创建一个epoll的句柄,当创建好epoll句柄后,它就是会占用一个fd值,使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。成功返回一个文件描述符,失败返回-1。

2、 int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

  • 对 epfd实例当中的 fd文件描述符 进行 op(添加,删除,更改)epoll_event 动作行为。这个函数是epoll的事件注册函数
  • op:
    EPOLL_CTL_ADD:注册新的fd到epfd中;
    EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
    EPOLL_CTL_DEL:从epfd中删除一个fd;
 typedef union epoll_data 
 {
        void        *ptr;
        int          fd;
        uint32_t     u32;
        uint64_t     u64;
} epoll_data_t;

struct epoll_event 
{
        uint32_t     events;      /* 感兴趣的事件,Epoll events  位图 */
        epoll_data_t data;        /* User data variable */
};
EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里

3、 int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

  • 等待事件的产生

实验:改进上面的用状态机实现的中继模型,加入多路IO转接机制(epoll)

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
#include <fcntl.h>
//#include <sys/select.h>
//#include <poll.h>
#include <sys/epoll.h>

#define TTY1 "/dev/tty11"
#define TTY2 "/dev/tty12"
#define BUFSIZE 1024

enum
{
	STATE_R = 1,
	STATE_W,
STATE_AUTO,
	STATE_Ex,
	STATE_T
};

struct fsm_st
{
	int state;
	int sfd;
	int dfd;
	char buf[BUFSIZE];
	int len;
	int pos;
	char* errstr;
};

static void fsm_driver(struct fsm_st *fsm)
{
	int ret;

	switch(fsm->state)
	{
		case STATE_R:
			fsm->len = read(fsm->sfd,fsm->buf,BUFSIZE);
			if(fsm->len == 0)
				fsm->state = STATE_T;
			else if(fsm->len < 0)
			{
				if(errno == EAGAIN)
					fsm->state = STATE_R;

				else
				{
					fsm->errstr = "read()";
					fsm->state = STATE_Ex;
				}	
					
			}
			else
			{		
				fsm->pos = 0;
				fsm->state = STATE_W;
			}
		
		break;

		case STATE_W:
			ret = write(fsm->dfd,fsm->buf+fsm->pos,fsm->len);
			if(ret < 0)
			{
				if(errno == EAGAIN)
					fsm->state = STATE_W;
				else
				{
					fsm->errstr = "read()";
					fsm->state = STATE_Ex;
				}
				
			}
			else
			{
				fsm->pos += ret;
				fsm->len -= ret;
				if(fsm->len == 0)
					fsm->state = STATE_R;
				else 
					fsm->state = STATE_W;
			}
		break;

		case STATE_Ex:
			perror(fsm->errstr);
			fsm->state = STATE_T;		
		break;

		case STATE_T:
			/* do something*/
		break;
		
		default:
			abort();
		break;
	}

}

static void relay(int fd1,int fd2)
{
	int fd1_save,fd2_save;
	struct fsm_st fsm12,fsm21;
	struct epoll_event ev;
	int epfd;

	fd1_save = fcntl(fd1,F_GETFL);
	fcntl(fd1,F_SETFL,fd1_save|O_NONBLOCK);

	fd2_save = fcntl(fd2,F_GETFL);
	fcntl(fd2,F_SETFL,fd2_save|O_NONBLOCK);

	fsm12.state = STATE_R;
	fsm12.sfd = fd1;
	fsm12.dfd = fd2;

	fsm21.state = STATE_W;
	fsm21.sfd = fd2;
	fsm21.dfd = fd1;	
	
//创建一个epoll的句柄
	epfd = epoll_creat(10);
	if(epfd < 0)
	{
		perror("epoll_creat()");
		exit(1);
	}
	
//添加需要监视的文件描述符
	ev.events = 0;
	ev.data.fd = fd1;
	epoll_ctl(epfd,EPOLL_CTL_ADD,fd1,&ev);

	ev.events = 0;
	ev.data.fd = fd2;
	epoll_ctl(epfd,EPOLL_CTL_ADD,fd2,&ev);
	
	while(fsm12.state != STATE_T || fsm21.state != STATE_T)	
	{
	//针对文件描述符fd1  布置监视现场
		ev.data.fd = fd1;
		ev.events = 0;

		if(fsm12.state == STATE_R)
			ev.events |= EPOLLIN;
		if(fsm21.state == STATE_W)
			ev.events |= EPOLLOUT;
		epoll_ctl(epfd,EPOLL_CTL_MOD,fd1,&ev);
	

		//针对文件描述符fd2  布置监视现场
		ev.data.fd = fd2;
		ev.events = 0;
		
		if(fsm12.state == STATE_W)
			ev.events |= EPOLLOUT;
		if(fsm21.state == STATE_R)
			ev.events |= EPOLLIN;
		epoll_ctl(epfd,EPOLL_CTL_MOD,fd2,&ev);
		
		if(fsm12.state < STATE_AUTO || fsm21.state < STATE_AUTO)
		{
			//监视fd1 fd2
			while(epoll_wait(epfd,&ev,1,-1) < 0)	
			{
				if(errno == EINTR)
					continue;

				perror("epoll_wait()");
				exit(1);
			}
		}
	
	//根据监视结果做出相应动作
		if( ev.data.fd == fd1 && ev.events & EPOLLIN || ev.data.fd == fd2 && ev.events & EPOLLOUT  || fsm12.state > STATE_AUTO)
		fsm_driver(&fsm12);

		if( ev.data.fd == fd1 && ev.events & EPOLLOUT|| ev.data.fd == fd2 && ev.events & EPOLLIN || fsm12.state > STATE_AUTO)
		fsm_driver(&fsm21);
	}

	fcntl(fd1,F_SETFL,fd1_save);
	fcntl(fd2,F_SETFL,fd2_save);
	
	close(epfd);
}

int main()
{

	int fd1,fd2;

	fd1 = open(TTY1,O_RDWR);
	if(fd1 < 0)
	{

		perror("open()");
		exit(1);
	}

	fd2 = open(TTY2,O_RDWR|O_NONBLOCK);
	if(fd1 < 0)
	{
		perror("open()");
		exit(1);
	}

	relay(fd1,fd2);


	close(fd2);
	close(fd1);
	

}

9 其他读写相关函数

readv和writev
向多个地址中读或写。

10 存储映射

mmap()函数
将一块内存或者是某一个文件的存储内容 映射到当前进程空间里面来。结果就是 我们在当前进程中访问目标空间中的数据。

int munmap(void *addr, size_t length);

  • addr: 目标内存空间 放到当前进程空间的起始地址,若为空,则函数自己找可用的位置

  • length:需要映射目标空间的长度

  • port : 映射过来的内存属性,即可以对该内存做什么操作,即映射后的操作权限
    PROT_EXEC Pages may be executed.
    PROT_READ Pages may be read.
    PROT_WRITE Pages may be written.
    PROT_NONE Pages may not be accessed.

  • flags :标记,特殊要求,位图
    必选:
    MAP_SHARED : 进程对进程对映射过来的内存修改,会同步到真实的内存空间。
    MAP_PRIVATE:进程对映射过来的内存修改,只是改动当前进程空间中目标内存空间,不会同步到真实的内存空间
    可选:
    如:MAP_ANONYMOUS 匿名映射,当前映射不依赖于任何文件(fd选项为-1),类似于 malloc()功能,空间会被初始化为0

  • fd : 文件描述符,如 需要映射一个文件过来,那么需要先将目标文件打开。

  • offset :偏移量
    从 fd 文件的 offset偏移量开始映射,映射length长度的空间到当前进程空间的addr地址,空间权限设置为port ,特殊要求是flags

void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);

  • 解除映射
  • 对 addr地址处 映射过来的空间(大小为length) 进行解除映射
  • 如果 mmap()的时候 flags 有MAP_ANONYMOUS 匿名映射要求,则此处munmap() 类似于 free()

实验1 :
查看一段内存空间中有多少 ‘a’。 用mmap()将目标空间映射过来,并做查找处理。

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/mman.h>

int main(int argc,char *argv[])
{
	int fd;
	struct stat statres;
	char *str;
	int i,count=0;
	
	if(argc < 2)
	{
		fprintf(stderr,"Usage...n");
		exit(1);
	}

	fd = open(argv[1],O_RDONLY);
	if(fd < 0)
	{
		perror("open()");
		exit(1);
	}
	

	if(fstat(fd,&statres) < 0)
	{
		perror("fstat()");
		exit(1);
	}
	str = mmap(NULL,statres.st_size,PROT_READ,MAP_SHARED,fd,0);
	if(str == MAP_FAILED)
	{
		perror("mmap()");
		exit(1);
	}

	close(fd);

	for(i = 0; i < statres.st_size; i++)
	{
		if(str[i] == 'a')
			count ++;
	}

	printf("%dn",count);


	munmap(str,statres.st_size);
}

实验2:父子进程之间的通信,mmap()实现共享内存。

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <string.h>

#define MEMSIZE 1024	

int main(int argc,char *argv[])
{
	char *ptr;
	pid_t pid;		

//不依赖于任何文件fd, 相当于 malloc()一段空间后映射过来,不依赖任何文件 所以fd==-1
	ptr = mmap(NULL,MEMSIZE,PROT_READ | PROT_WRITE,MAP_SHARED|MAP_ANONYMOUS,-1,0);
	if(ptr == MAP_FAILED)
	{
		perror("mmap()");
		exit(1);
	}


	pid = fork();//先mmap  后fork
	if(pid < 0)
	{
		perror("fork()");
		munmap(ptr,MEMSIZE);
		exit(1);
	}

	if(pid == 0) //Child write 
	{
		strcpy(ptr,"Hello!");
		munmap(ptr,MEMSIZE);
		exit(0);
	}
	else //Parent read
	{
		wait(NULL);
		puts(ptr);//puts()将字符串s和末尾的换行符写入标准输出。
		munmap(ptr,MEMSIZE);
		exit(0);
	}


}

11 文件锁

fcntl();
lockf();
flock();

int lockf(int fd, int cmd, off_t len);

  • fd:需要锁的目标文件
  • len: 要锁多长,0表示 文件有多长 锁多长,即加锁到文件末端,就算文件加长 也会随之锁上
  • cmd :实现的命令
    F_LOCK 解锁,阻塞式加锁
    F_TLOCK 尝试加锁,非阻塞式加锁
    F_ULOCK 解锁
    F_TEST 测试有没有锁

注意:
给一个文件加锁,参数指定fd,通过文件描述符给文件加锁,加锁是加到了文件本身,即inode层面,并不是每个fd所对应的文件属性结构体。注意:当一个进程打开两次同一个文件的时候,如图中所用的第一个和第三个,指向不同的文件属性结构体,但是都是指向同一个 inode文件,这种情况 如果一个加锁后,另一个执行close()文件,会造成加锁的文件被意外解锁。
在这里插入图片描述

实验:多进程并发,实现20个进程操作同一个文件,每个进程打开+1 。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <pthread.h>
#include <unistd.h>

#define PROCNUM 20
#define FNAME "/home/mhr/Desktop/xitongbiancheng/super_io/out"
#define LINESIZE 1024


static void func_add(void)
{
	FILE *fp;
	int fd;
	char linebuf[LINESIZE];

	fp = fopen(FNAME,"r+");
	if(fp == NULL)
	{
		perror("fopen()");
		exit(1);
	}

fd = fileno(fp);//返回文件描述符
//if(fd < 0)

//上锁,加锁到文件末端
lockf(fd,F_LOCK,0);

	fgets(linebuf,LINESIZE,fp);
	fseek(fp,0,SEEK_SET);

	//sleep(1);	
	
//文件的全缓冲,而fprintf是航缓冲,所以解锁前没有close() 写操作就不能将数据写到文件中,需要ffllush()刷新流
	fprintf(fp,"%dn",atoi(linebuf)+1);
	fflush(fp);//刷新
//解锁
lockf(fd,F_ULOCK,0);
	
//解锁后 close()  防止意外解锁
	fclose(fp);
return;

} 

int main()
{
	int i,err;
	pid_t pid;

	for(i = 0; i < PROCNUM; i++)
	{
		pid = fork();//创建20个子进程
		if(pid < 0)
		{
			perror("fork()");
			exit(1);
		}
		
		if(pid == 0)//Child 
		{
			func_add();
			exit(0);//子进程操作后就结束
		}

	}

	for(i = 0;i < PROCNUM; i++)
	{
		wait(NULL);//父进程收尸
	}

	exit(0);
	
}

最后

以上就是甜蜜楼房为你收集整理的LINUX系统编程--7 高级IO七 高级IO的全部内容,希望文章能够帮你解决LINUX系统编程--7 高级IO七 高级IO所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(36)

评论列表共有 0 条评论

立即
投稿
返回
顶部