【APUE】高级I/O

目录

一、五大 IO 模型

1.1 完整的 IO 过程

1.2 阻塞 IO

1.3 非阻塞 IO

1.4 信号驱动式 IO

1.5 多路转接

1.6 异步 IO

二、有限状态机编程

2.1 基本思想

2.2 数据中继模型

2.3 数据中继实现

2.4 中继引擎实现

三、IO多路转接

3.1 select

3.2 poll

3.3 epoll 

3.3.1 epoll_create

3.3.2 epoll_ctl

3.3.3 epoll_wait

3.3.4 代码示例

四、其他读写函数

4.1 readv

4.2 writev

五、存储映射 IO

5.1 简介

5.2 共享内存用作进程间通信

六、文件锁

6.1 lockf

6.2 flock

6.3 fcntl


一、五大 IO 模型

1.1 完整的 IO 过程

进程发起 IO 调用请求,然后由内核执行 IO。内核执行 IO 又包括两个阶段(以从设备读取数据为例):

  • 数据准备阶段:内核等待 I/O 设备获取数据,并将数据填至内核缓冲区
  • 数据拷贝阶段:将数据从内核缓冲区拷贝到用户进程缓冲区

上述整个过程如图所示

根据数据准备阶段及数据拷贝阶段的行为不同,可以分为如下五类IO模型:

  • 阻塞 IO
  • 非阻塞 IO
  • 信号驱动式 IO
  • 多路转接
  • 异步 IO

1.2 阻塞 IO

在内核准备数据过程中,进程一直阻塞等待

1.3 非阻塞 IO

如果内核还未将数据准备完毕,系统调用仍然会直接返回 EWOULDBLOCK 错误码。非阻塞 IO 往往需要反复查看数据准备完毕没有,这个过程称为轮询

1.4 信号驱动式 IO

当进程发起一个 IO 操作,会向内核注册一个信号处理函数,然后进程不阻塞直接返回;待内核将数据准备完毕时,主动使用 SIGIO 信号通知进程,进程在信号处理函数中发起 IO 调用

1.5 多路转接

和阻塞 IO 类似,在内核准备数据过程中,进程也会阻塞等待。不过是同时等待多个文件描述符的数据准备状态

1.6 异步 IO

内核在数据拷贝完成时再通知应用程序。在此期间应用程序继续执行,不阻塞

如果用拟人化的比喻,那么: 

  • 阻塞 IO:啥别的事也不做,一直盯着鱼竿,直到鱼上钩就钓
  • 非阻塞 IO:边看手机边钓鱼,需要时不时看看鱼上钩没,上钩就钓
  • 信号驱动式 IO:在鱼竿上放个铃铛,然后干别的事,直到听到铃铛响,说明上钩,钓
  • 多路转接:一次带来几百个鱼竿钓,盯着这一堆鱼竿,哪个上钩就钓哪个
  • 异步IO:让别人帮自己钓鱼,自己干别的事儿就行,别人钓到鱼了直接给你

二、有限状态机编程

是一种编程思想,非常适合用来解决需要非结构化程序控制流程才能解决的问题

  • 结构化程序控制流程:程序应该有清晰、易于理解的控制结构,通常由顺序执行、条件分支和循环控制结构组成。这些结构化元素使得程序的流程易于跟踪,逻辑清晰
  • 非结构化程序控制流程:非结构化的程序流程则没有遵循这种严格的、有序的控制流。在非结构化的程序中,流程控制可能大量依赖于跳转语句(如 goto),这使得程序的执行路径不那么明显,难以追踪

2.1 基本思想

有限状态机(FSM)的思想就像是玩“红绿灯”游戏一样。在这个游戏里,你可以是“停止”状态,也可以是“行走”状态。如果现在是红灯,你就得停下来;如果是绿灯,你就可以走。当红灯变绿灯时,你从“停止”状态变到“行走”状态,而当绿灯变红灯时,你又得从“行走”状态变回“停止”状态。就这么简单!

当我们用这个思想来解决问题时,通常会遵循这样的流程:

  1. 确定状态:首先,你得知道都有哪些状态。就像红绿灯游戏,只有“停止”和“行走”两种状态
  2. 确定事件:然后,你要弄清楚有哪些事件。在红绿灯游戏里,有等变红和等变绿两个事件
  3. 制定规则:接下来,你要制定规则,规则决定了事件如何让状态变化。比如,当红灯亮起时,你就得停下;当绿灯亮起时,你就可以走
  4. 执行状态转换:最后,根据你的规则,当事件发生时,你就改变状态。就像你在游戏里看到绿灯就开始走

使用有限状态机的思想来解决问题,就是按这个方法来一步步设计你的系统,这样你就可以清楚地知道在任何时候系统应该做什么,以及它将如何响应不同的事件。这种方法让问题变得简单,因为你一次只处理一个状态和几个事件,就像一个接一个地走红绿灯一样

接下来,我们使用有限状态机的思想,结合非阻塞 IO 完成一个数据中继的编程实例

2.2 数据中继模型

数据中继可以理解为一个数据中转站。如下图所示,两两用户之间进行数据交互需要通过服务器,由服务器来做这个数据中转。如何中转是我们需要研究的问题

将这个模型进行简化抽象,我们发现两两用户之间的数据交互其实就是双方的一个数据交换。逻辑上需要做的就是不断执行如下两件事:

  • 从 tty1 中读取数据,写入 tty2
  • 从 tty2 中读取数据,写入 tty1

显然,上述的读取写入涉及到了 IO 操作。显然,应该用非阻塞 IO。因为如果用阻塞的 IO,那么当 tty1 没有数据但是 tty2 有数据时,进程可能一直阻塞在“读 tty1”的阶段,就很呆(tty 代表设备)

2.3 数据中继实现

需求:有左右两个设备,第一个任务为读左设备,写右设备;第二个任务读右设备,写左设备

这两个任务本质上都是读取设备并写入另一个设备。用有限状态机的思想分析任务的状态、事件和规则,可以绘制出如下状态转移图

上图中,红色框代表任务可能的状态,连接各状态之间的箭头代表了可能的事件及状态转移规则。基于这个状态转移图,我们来实现我们的代码

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

#define TTY1 "/dev/pts/1"
#define TTY2 "/dev/pts/2"

#define BUFSIZE 1024

enum
{
	STATE_R = 1,    // 读取态
	STATE_W,        // 写入态
	STATE_Ex,       // 异常error态
	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->state = STATE_Ex;  // 真错,转移至异常态
					fsm->errstr = "error in read()";
				}
			}
			else {
				fsm->state = STATE_W;   // 读取成功,转移至写态
				fsm->pos = 0;   // 此时buf中所有内容都还没被写入,pos置0
			}
			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->state = STATE_Ex;  // 真错,转移至异常态
					fsm->errstr = "error in write()";
				}
			}
			else
			{
				fsm->pos += ret;        // 写入了ret字节
							// 尚未被写入内容的起始位置后移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 sth */
			break;
		default:
			abort();
			break;
	}
}

static void relay(int fd1, int fd2) {

	// 将文件描述符fd1与fd2都设置为“以非阻塞形式打开”
	int fd1_save = fcntl(fd1, F_GETFL);
	fcntl(fd1, F_SETFL, fd1_save|O_NONBLOCK);
	int fd2_save = fcntl(fd2, F_GETFL);
	fcntl(fd2, F_SETFL, fd2_save|O_NONBLOCK);

	struct fsm_st fsm12, fsm21;     // 两个任务
					// 需要两个状态转移过程
					// fsm12维护从fd1读写入fd2的状态转移过程
					// fsm21维护从fd2读写入fd1的状态转移过程
	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, 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);
	}
	write(fd1, "TTY1\n", 5);

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

	relay(fd1, fd2);        // 数据交换的双方为fd1和fd2

	close(fd2);
	close(fd1);

	exit(0);
}

为了测试我们的代码,我们打开三个终端,其中一个终端运行我们的 ./a.out 进程,另外两个终端作为我们数据交换的两个设备。可通过命令 tty 显示当前终端用的哪个虚拟控制台

可以看到,数据交换成功! 

上述代码存在忙等现象,会使得CPU利用率占满,原因在于如下代码:

while(fsm12.state != STATE_T || fsm21.state != STATE_T) {
    fsm_driver(&fsm12);
    fsm_driver(&fsm21);
}

如果设备没有准备好数据,则进入 fsm_driver 后,执行 read 调用时,内核立即会返回(非阻塞),是一个假错,执行:

if(errno == EAGAIN) { // 通常在执行非阻塞io时引发EAGAIN,这意味着“现在没有可用的数据,以后再试一次” 。
    fsm->state = STATE_R;
}

状态不变,跳出 case 语句和驱动函数后,继续循环,所以导致 cpu 利用率高

2.4 中继引擎实现

在数据中继实现中,我们实现了两个设备进行数据交换的例子。现在我们想实现管理 100 对设备两两交换的中继引擎

我们将代码封装成库,并在 main.c 中模拟用户使用库函数的过程。详细实现如下(呜呜呜😆~~~这下是在没有任何教学的情况下亲自手把手写的,泪目!但是给自己点赞!!!😀) 


共编写三个代码文件,relayer.h、relayer.c 和 main.c,详细内容及含义见下

relayer.h,主要描述了提供给用户的接口,用户能够看到 

#ifndef RELAYER_H__
#define RELAYER_H__

#define JOBMAX 100
enum
{
        STATE_RUNNING = 1,      // 任务运行中
        STATE_CANCELED,         // 任务被取消
        STATE_OVER              // 任务完成
};      // 描述单个任务的状态

struct rel_job_user_st  // 暴露给用户的描述job的结构体
{
        int state;      // 该任务的状态
        int fd1;        // 该任务交互的双方
        int fd2;
        time_t start;   // 任务起始时间(s)
        time_t end;     // 任务终止时间(s)
};

// 创建描述任务的结构体,并存放在任务数组的某个位置
int rel_addjob(int fd1, int fd2);
/* return >= 0  成功,返回描述任务的结构体存放在数组的哪个下标(作为任务ID)
 *        == -EINVAL    失败,参数非法
 *        == -NOSPC     失败,任务数据满
 *        == -ENOMEM    失败,内存不足
 */

// 取消一个任务
int rel_canceljob(int id);
/* return == 0  成功,指定任务成功取消
 *        == -EINVAL    失败,参数非法
 *        == -EBUSY     失败,任务已处于非运行态,无需取消
 */

// 回收一个非运行态的任务
int rel_waitjob(int id, struct rel_job_user_st *);
/* return == 0  成功,指定任务已终止,并顺利收尸
 *        == -EINVAL    失败,参数非法
 */

#endif

relayer.c,主要描述了接口的具体实现,对用户隐藏 

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <unistd.h>
#include <pthread.h>
#include <string.h>
#include "relayer.h"

#define BUFSIZE 1024

enum
{
        STATE_R = 1,    // 读取态
        STATE_W,        // 写入态
        STATE_Ex,       // 异常error态
        STATE_T         // 终止态
};

struct fsm_st
{
        int state;      // 状态
        int sfd;        // 源文件
        int dfd;        // 目标文件
        char buf[BUFSIZE];      // 缓冲区
        int len;        // 记录缓冲区中的字节数
        int pos;        // 记录尚未被写入内容的起始位置
        char * errstr;  // 记录出错信息
};

struct rel_job_st       // 真正描述job的结构体
{
        int state;
        int fd1, fd1_save;
        int fd2, fd2_save;
        struct fsm_st fsm12, fsm21;
        time_t start;
        time_t end;
        pthread_mutex_t mutex;  // 保证互斥访问结构体的成员
};

static struct rel_job_st* job[JOBMAX];
static pthread_mutex_t mutex_job = PTHREAD_MUTEX_INITIALIZER;   // 保证互斥访问job数组
static pthread_once_t init_once = PTHREAD_ONCE_INIT;
static pthread_t tid;   // 不断驱动状态转换的线程

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->errstr = "error in read()";
                                }
                        }
                        else {
                                fsm->state = STATE_W;   // 读取成功,转移至写态
                                fsm->pos = 0;   // 此时buf中所有内容都还没被写入,pos置0
                        }
                        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->state = STATE_Ex;  // 真错,转移至异常态
                                        fsm->errstr = "error in write()";
                                }
                        }
                        else
                        {
                                fsm->pos += ret;        // 写入了ret字节
                                                        // 尚未被写入内容的起始位置后移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 sth */
                        break;
                default:
                        abort();
                        break;
        }
}

static void* thr_relayer(void*p) {      // 不断推动状态的线程
                                        // 该线程会不断访问job数组及访问job数组中的结构体中的成员
                                        // 我们需要引入互斥量
        while (1) {
                pthread_mutex_lock(&mutex_job); // 访问job前先上锁
                for (int i = 0; i < JOBMAX; ++i)
                {
                        if (job[i] != NULL) {
                                pthread_mutex_lock(&job[i]->mutex);     // 访问结构体的成员前先上锁
                                if (job[i]->state == STATE_RUNNING)
                                {
                                        fsm_driver(&job[i]->fsm12);
                                        fsm_driver(&job[i]->fsm21);
                                        if (job[i]->fsm12.state == STATE_T && job[i]->fsm21.state == STATE_T)
                                                job[i]->state = STATE_OVER;
                                }
                                pthread_mutex_unlock(&job[i]->mutex);
                        }
                }
                pthread_mutex_unlock(&mutex_job);
        }
}

static void module_unload(void) {
        pthread_cancel(tid);    // 终止不断推动状态机的线程
        pthread_join(tid, NULL);        // 收尸
        for (int i = 0; i < JOBMAX; ++i) {      // 这下可以安安心心,无需上锁访问数组中的结构体的成员
                if (job[i] != NULL) {
                        fcntl(job[i]->fd1, F_SETFL, job[i]->fd1_save);
                        fcntl(job[i]->fd2, F_SETFL, job[i]->fd2_save);
                        free(job[i]);
                }
        }
}

static void module_load(void)   // 创建出那个不断推动状态机的线程
{
        int err = pthread_create(&tid, NULL, thr_relayer, NULL);
        if (err)
        {
                fprintf(stderr, "pthread_create():%s\n", strerror(err));
                exit(1);
        }
        atexit(module_unload);
}

static int get_free_pos_unlocked()
{
        for (int i = 0; i < JOBMAX; ++i)
        {
                if (job[i] == NULL)
                        return i;
        }
        return -1;
}

int rel_addjob(int fd1, int fd2){

        struct rel_job_st *me;
        pthread_once(&init_once,module_load);   // 创建一个中继驱动,不断推动各任务状态机

        me = malloc(sizeof(*me));
        if (me == NULL)
                return -ENOMEM;
        // 初始化任务
        pthread_mutex_init(&me->mutex, NULL);

        me->fd1 = fd1;
        me->fd2 = fd2;
        me->state = STATE_RUNNING;
        me->start = time(NULL);
        me->end = time(NULL);
        me->fd1_save = fcntl(me->fd1, F_GETFL);
        fcntl(me->fd1, F_SETFL, me->fd1_save|O_NONBLOCK);
        me->fd2_save = fcntl(me->fd2, F_GETFL);
        fcntl(me->fd2, F_SETFL, me->fd2_save|O_NONBLOCK);

        me->fsm12.sfd = me->fd1;
        me->fsm12.dfd = me->fd2;
        me->fsm12.state = STATE_R;

        me->fsm21.sfd = me->fd2;
        me->fsm21.dfd = me->fd1;
        me->fsm21.state = STATE_R;

        pthread_mutex_lock(&mutex_job);
        // 访问job数组前需要加锁
        int pos = get_free_pos_unlocked();
        if (pos < 0)
        {
                pthread_mutex_unlock(&mutex_job);       // 别忘了解锁
                fcntl(me->fd1, F_SETFL, me->fd1_save);  // 恢复文件描述符行为
                fcntl(me->fd2, F_SETFL, me->fd2_save);
                free(me);
                return -ENOSPC;
        }
        job[pos] = me;
        pthread_mutex_unlock(&mutex_job);

        return pos;
}

int rel_canceljob(int id) {
        pthread_mutex_lock(&mutex_job); // 访问job数组前需要加锁
        if (job[id] == NULL) {
                pthread_mutex_unlock(&mutex_job);
                return -EINVAL;
        }
        pthread_mutex_lock(&job[id]->mutex);    // 访问job数组中的结构体中的成员需要加锁
        if (job[id]->state == STATE_OVER || job[id]->state == STATE_CANCELED) { // 非运行态的任务无需取消
                pthread_mutex_unlock(&job[id]->mutex);
                pthread_mutex_unlock(&mutex_job);
                return -EBUSY;
        }
        job[id]->state = STATE_CANCELED;        // 置为取消
        job[id]->end = time(NULL);      // 记录被取消的时间
        pthread_mutex_unlock(&job[id]->mutex);
        pthread_mutex_unlock(&mutex_job);
        return 0;
}

int rel_waitjob(int id, struct rel_job_user_st * jobinfo) {
        pthread_mutex_lock(&mutex_job); // 访问job数组,加锁
        pthread_mutex_lock(&job[id]->mutex);    // 访问数组中的结构体中的成员,加锁
        if (job[id] == NULL || job[id]->state == STATE_RUNNING) {
                pthread_mutex_unlock(&job[id]->mutex);
                pthread_mutex_unlock(&mutex_job);
                return -EINVAL;
        }
        jobinfo->state = job[id]->state;        // 将任务的信息存入暴露给用户的描述任务的结构体,返回给用户
        jobinfo->fd1 = job[id]->fd1;
        jobinfo->fd2 = job[id]->fd2;
        jobinfo->start = job[id]->start;
        jobinfo->end= job[id]->end;
        fcntl(job[id]->fd1, F_SETFL, job[id]->fd1_save);        // 恢复fd默认行为
        fcntl(job[id]->fd2, F_SETFL, job[id]->fd2_save);
        pthread_mutex_unlock(&job[id]->mutex);
        free(job[id]);
        pthread_mutex_unlock(&mutex_job);
        return 0;
}

main.c,模拟用户使用接口的过程 

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

#define TTY1 "/dev/tty2"
#define TTY2 "/dev/tty3"
#define TTY3 "/dev/tty4"
#define TTY4 "/dev/tty5"

#define BUFSIZE 1024

int main() {
        int fd1, fd2;

        fd1 = open(TTY1, O_RDWR);
        if (fd1 < 0)
        {
                perror("open()");
                exit(1);
        }
        write(fd1, "TTY1\n", 5);

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

        int job1 = rel_addjob(fd1, fd2);        // 数据交换的双方为fd1和fd2
        if (job1 < 0) {
                fprintf(stderr, "rel_addjob():%s\n", strerror(-job1));
        }

        int fd3, fd4;

        fd3 = open(TTY3, O_RDWR);
        if (fd3 < 0)
        {
                perror("open()");
                exit(1);
        }
        write(fd3, "TTY3\n", 5);

        fd4 = open(TTY4, O_RDWR);
        if (fd4 < 0)
        {
                perror("open()");
                exit(1);
        }
        write(fd4, "TTY4\n", 5);

        int job2 = rel_addjob(fd3, fd4);        // 数据交换的双方是fd3和fd4
        if (job2 < 0) {
                fprintf(stderr, "rel_addjob():%s\n", strerror(-job2));
        }

        sleep(60);      // 休眠60s

        int err;
        err = rel_canceljob(job2);      // 取消任务2
        if (err < 0)
                fprintf(stderr, "rel_canceljob():%s\n", strerror(err));

        struct rel_job_user_st * info;
        info = malloc(sizeof(*info));
        err = rel_waitjob(job2, info);  // 收尸任务2
        if (err < 0)
                fprintf(stderr, "rel_waitjob():%s\n", strerror(err));
        // 打印一些job2的终止信息
        fprintf(stdout, "job2's end state is %d, keeps running %lds\n", info->state, info->end-info->start);

        sleep(30);      // 休眠20s
        // 取消任务1及收尸任务1,错误校验及打印收尸信息略
        rel_canceljob(job1);
        rel_waitjob(job1, info);

        close(fd4);
        close(fd3);
        close(fd2);
        close(fd1);

        exit(0);
}

接下来我们开始测试

由上可以看出,数据中继引擎功能基本实现 


三、IO多路转接

IO 多路转接模型核心思路:系统给我们提供一类函数(select、poll、epoll 函数),它们可以同时监控多个文件描述符的数据准备状态,任何一个返回内核数据准备完毕,应用进程再发起 recvfrom 系统调用


3.1 select

古老的函数,可移植性好,但是有很多缺陷。man 2 select

#include <sys/select.h>

int select(int nfds, fd_set *readfds, fd_set *writefds,
           fd_set *exceptfds, struct timeval *timeout);

功能:监视文件描述符的可读写状态及异常状态 

  • nfds — 传入该形参的实参值应为:待监视的一堆文件描述符中,值最大的那个文件描述符(别忘了文件描述符本质上为整型值)的值再加一
  • readfds — 指向文件描述符集。如果该文件描述符集中有文件描述符可读了,select 即返回
  • writefds — 指向文件描述符集。如果该文件描述符集中有文件描述符可写了,select 即返回
  • exceptfds — 指向文件描述符集。如果该文件描述符集中有文件描述符异常了,select 即返回
  • 若 readfds 中文件描述符无一可读 writefds 中文件描述符无一可写 exceptfds 中文件描述符无一异常,则一直阻塞
  • timeout — 微秒级的超时设置。该函数最多阻塞的时间为 timeout 所指定的时间
  • 成功则返回 readfds 中可读的文件描述符个数 + writefds 中可写的文件描述符个数 + exceptfds 中异常的文件描述符个数,并仅在 readfds 中保留可读的文件描述符、在 writefds 中保留可写的文件描述符、在 exceptfds 保留异常的文件描述符;失败则返回 -1,并设置 errno,且此时传入函数的那三个文件描述符集中的内容变得不可预知

上面涉及到了“文件描述符集”,用来表示一堆文件描述符所构成的集合,在这里表示这些文件描述符是“被监视的”。诶?之前是不是遇到过类似的东东?对!信号集也是类似的概念,只不过信号集是用来表示一堆信号所构成的集合。操作文件描述符集的相关调用如下

#include <sys/select.h>

void FD_CLR(int fd, fd_set *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);    // 清空文件描述符集set
  • fd — 代表某个文件描述符
  • set — 指向某个 fd_set 类型的文件描述符集 

代码示例:重构 2.3 中的代码

需求:避免忙等现象的发生

之前的代码会出现忙等现象

究其原因是因为不断运行的 while 循环占用了 CPU:

while(fsm12.state != STATE_T || fsm21.state != STATE_T) {
    fsm_driver(&fsm12);
    fsm_driver(&fsm21);
}

我们现在希望进行如下修改:

while(fsm12.state != STATE_T || fsm21.state != STATE_T) {
    // 布置监视任务
  
    // 监视
    select();

    // 查看监视结果
    
    if ()
        fsm_driver(&fsm12);
    if ()
        fsm_driver(&fsm21);
}

满足一定条件才进行状态推动,否则阻塞在 select,这样一来,while 循环就不会一直死等运行占用 CPU 了

代码实现如下,详见注释 

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

#define TTY1 "/dev/tty2"
#define TTY2 "/dev/tty3"

#define BUFSIZE 1024

enum
{
        STATE_R = 1,    // 读取态
        STATE_W,        // 写入态
        STATE_Ex,       // 异常error态
        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->state = STATE_Ex;  // 真错,转移至异常态
                                        fsm->errstr = "error in read()";
                                }
                        }
                        else {
                                fsm->state = STATE_W;   // 读取成功,转移至写态
                                fsm->pos = 0;   // 此时buf中所有内容都还没被写入,pos置0
                        }
                        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->state = STATE_Ex;  // 真错,转移至异常态
                                        fsm->errstr = "error in write()";
                                }
                        }
                        else
                        {
                                fsm->pos += ret;        // 写入了ret字节
                                fsm->len -= ret;        // 尚未被写入内容的起始位置后移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 sth */
                        break;
                default:
                        abort();
                        break;
        }
}

static void relay(int fd1, int fd2) {

        // 将文件描述符fd1与fd2都设置为“以非阻塞形式打开”
        int fd1_save = fcntl(fd1, F_GETFL);
        fcntl(fd1, F_SETFL, fd1_save|O_NONBLOCK);
        int fd2_save = fcntl(fd2, F_GETFL);
        fcntl(fd2, F_SETFL, fd2_save|O_NONBLOCK);

        struct fsm_st fsm12, fsm21;     // 两个任务
                                        // 需要两个状态转移过程
                                        // fsm12维护从fd1读写入fd2的状态转移过程
                                        // fsm21维护从fd2读写入fd1的状态转移过程
        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) {
                // 布置监视任务
                int nfds = fd1 > fd2 ? (fd1+1):(fd2+1);
                fd_set rset, wset;
                FD_ZERO(&rset); // 初始化待被监视可读状态的文件描述符集
                FD_ZERO(&wset); // 初始化待被监视可写状态的文件描述符集
                if (fsm12.state == STATE_R)     // 在驱动之前,我们希望监视fsm12任务所维护的两个文件描述符的可读写状态
                        FD_SET(fsm12.sfd, &rset);
                if (fsm12.state == STATE_W)
                        FD_SET(fsm12.dfd, &wset);
                if (fsm21.state == STATE_R)     // 在驱动之前,我们希望监视fsm21任务所维护的两个文件描述符的可读写状态
                        FD_SET(fsm21.sfd, &rset);
                if (fsm21.state == STATE_W)
                        FD_SET(fsm21.dfd, &wset);

                // 监视

                if (select(nfds, &rset, &wset, NULL, NULL) < 0)
                {
                        if (errno == EINTR)     // 假错,代表收到信号
                                continue;       // 因为select会改变传入的文件描述符集,因此需要重新布置监视任务
                        perror("select()");     // 真错
                        exit(1);
                }

                // 查看监视结果

                if (FD_ISSET(fsm12.sfd, &rset) || FD_ISSET(fsm12.dfd, &wset) || fsm12.state == 3)   // 当fsm12任务中的源fd可读或目标fd可写,才推动fsm12

            // 注意,当异常error态(3),也要无条件推动!!
                        fsm_driver(&fsm12);

                if (FD_ISSET(fsm21.sfd, &rset) || FD_ISSET(fsm21.dfd, &wset) || fsm21.state == 3)   // 当fsm21任务中的源fd可读或目标fd可写,才推动fsm21
                        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);
        }
        write(fd1, "TTY1\n", 5);

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

        relay(fd1, fd2);        // 数据交换的双方为fd1和fd2

        close(fd2);
        close(fd1);

        exit(0);
}

这样一来,如果终端所对应的文件描述符的可读写状态没有变化,就会阻塞,不会无意义地不断 while 循环。可以看到进程开始运行后 CPU 利用率没什么明显提升,无忙等现象了

思考一下 select 的缺陷有哪些?

  • 能监视的状态比较单一,只能监视文件描述符的可读写状态和异常状态(甚至还不能区分不同种类的异常)
  • 因为 nfds 形参类型限制,既无法监视值很大的文件描述符,也无法监视太多的文件描述符
  • 待被监视的文件描述符由传给形参的实参指定,监视结果也回填至传入的实参。相当于输入与输出共用同一片空间,输出会覆盖输入,很不方便

3.2 poll

可移植,也没那么多缺陷,应该重点掌握。man 2 poll

#include <poll.h>

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

功能:监视文件描述符的事件(即状态的改变)

  • fds — 指向存放 pollfd 结构体的数组的首地址。其中,一个 pollfd 结构体就代表了一个监视某文件描述符的某事件的任务。该结构体成员如下:
struct pollfd {
    int   fd;         /* file descriptor */
    short events;     /* requested events */
    short revents;    /* returned events */
};

// 其中,events和revents要用位图的视角看待。其所能取的宏值及含义详见man手册
  • nfds — 用于指定 fds 所指向的数组中结构体的个数
  • 但凡有一个 pollfd 所代表的任务监视到了其指定事件的发生,就返回;否则阻塞
  • timeout — 毫秒级的超时设置,该函数最多阻塞的时间为 timeout 所指定的时间。若为 0,则该函数变为非阻塞;若为负,则不设定阻塞时间的上限

代码示例:重构 3.1 中的代码。要求用 poll 替代 select

代码实现如下,详见注释

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

#define TTY1 "/dev/tty2"
#define TTY2 "/dev/tty3"

#define BUFSIZE 1024

enum
{
        STATE_R = 1,    // 读取态
        STATE_W,        // 写入态
        STATE_Ex,       // 异常error态
        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->state = STATE_Ex;  // 真错,转移至异常态
                                        fsm->errstr = "error in read()";
                                }
                        }
                        else {
                                fsm->state = STATE_W;   // 读取成功,转移至写态
                                fsm->pos = 0;   // 此时buf中所有内容都还没被写入,pos置0
                        }
                        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->state = STATE_Ex;  // 真错,转移至异常态
                                        fsm->errstr = "error in write()";
                                }
                        }
                        else
                        {
                                fsm->pos += ret;        // 写入了ret字节
                                fsm->len -= ret;        // 尚未被写入内容的起始位置后移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 sth */
                        break;
                default:
                        abort();
                        break;
        }
}

static void relay(int fd1, int fd2) {

        // 将文件描述符fd1与fd2都设置为“以非阻塞形式打开”
        int fd1_save = fcntl(fd1, F_GETFL);
        fcntl(fd1, F_SETFL, fd1_save|O_NONBLOCK);
        int fd2_save = fcntl(fd2, F_GETFL);
        fcntl(fd2, F_SETFL, fd2_save|O_NONBLOCK);

        struct fsm_st fsm12, fsm21;     // 两个任务
                                        // 需要两个状态转移过程
                                        // fsm12维护从fd1读写入fd2的状态转移过程
                                        // fsm21维护从fd2读写入fd1的状态转移过程
        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) {
                // 布置监视任务,这几行可以放在循环外
                struct pollfd pfd[2];
                pfd[0].fd = fd1;
                pfd[0].events = 0;
                pfd[1].fd = fd2;
                pfd[1].events = 0;
                // fms12是从fd1读数据,写入fd2
                // fms21是从fd2读数据,写入fd1
                if (fsm12.state == STATE_R)     // fsm12为可读态,监视fd1何时可读
                        pfd[0].events |= POLLIN;
                if (fsm12.state == STATE_W)     // fsm12为可写态,监视fd2何时可写
                        pfd[1].events |= POLLOUT;
                if (fsm21.state == STATE_R)     // fsm21为可读态,监视fd2何时可读
                        pfd[1].events |= POLLIN;
                if (fsm21.state == STATE_W)     // fsm21为可写态,监视fd1何时可写
                        pfd[0].events |= POLLOUT;

                // 监视
                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 == 3)                                                                                // fd1可读或者fd2可写或者状态机为异常态,无条件推动fsm12
                        fsm_driver(&fsm12);
                if (pfd[0].revents & POLLOUT || pfd[1].revents & POLLIN || fsm21.state == 3)                                                                                // fd1可写或者fd2可读或者状态机为异常态,无条件推动fsm21
                        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);
        }
        write(fd1, "TTY1\n", 5);

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

        relay(fd1, fd2);        // 数据交换的双方为fd1和fd2

        close(fd2);
        close(fd1);

        exit(0);
}

3.3 epoll 

LINUX 的方言,不可移植,理解即可。man 7 epoll 可以查看机制

可以将 epoll 想象成一个位于内核的监视工具。通过 epoll 监视文件描述符的流程如下:

  1. 通过 epoll_create 在内核创建一个 epoll 实例(相当于创建一个监视工具)
  2. 通过 epoll_ctl 向 epoll 实例中添加/修改/删除监视任务
  3. 通过 epoll_wait 等待其所监视的文件描述符上的事件
  4. 当不再需要 epoll 实例时,需要通过 close 关闭它

select、poll 都需要在用户态确定希望被监视的 fd 的集合。然后在监视的时候需要将该集合复制到内核空间中,这样内核才能帮助我们轮询 fd,这个过程具有一定开销;而 epoll 直接往内核去添加希望被监视的 fd,去除了复制过程的开销

select、poll 都需要不断遍历所有的 fd 来获取就绪的文件描述符,时间复杂度高;而 epoll 在文件就绪后会触发回调,然后将就绪的 fd 放入就绪链表中。这样一来,只需要从就绪链表中获取就绪的文件描述符,时间复杂度低


3.3.1 epoll_create

man 2 epoll_create

#include <sys/epoll.h>

int epoll_create(int size);

功能:创建一个 epoll 实例(即创建一个监视工具)

  • size —  LINUX 2.6.8 以后,size 无意义了,只要传一个正数即可
  • 成功返回一个文件描述符,用于表征该 epoll 实例;失败返回 -1 并设置 errno

3.3.2 epoll_ctl

man 2 epoll_ctl

#include <sys/epoll.h>

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

功能:向 epoll 实例中添加/修改/删除监视任务

  • epfd — 这是 epoll_create 成功后所返回的文件描述符,表征一个 epoll 实例(监视工具)
  • fd — 指定一个文件描述符
  • op — 指定 epoll_ctl 的具体行为。其值及含义如下
含义
EPOLL_CTL_ADD注册监视任务:让监视工具开始监视 fd。默认监视的事件为 event 所指定的事件。可以往一个监视工具上注册多个监视任务
EPOLL_CTL_MOD修改监视任务:修改监视 fd 上的事件为 event 所指定的事件
EPOLL_CTL_DEL取消监视任务:让监视工具停止监视 fd
  • event —  指向 struct epoll_event 类型的结构体,表示要监视的事件。结构体的成员如下:
struct epoll_event {
    uint32_t     events;      /* Epoll events */
    epoll_data_t data;        /* User data variable */
};

typedef union epoll_data {
    void        *ptr;
    int          fd;
    uint32_t     u32;
    uint64_t     u64;
} epoll_data_t;


// 其中,events要用位图的视角看待。其所能取的宏值及含义详见man手册
// data是epoll_data_t类型的结构体,可由用户自定义其字段的含义,常用来储存一些与监视任务有关的信息

附:epoll 实例中的任务的组织形式

3.3.3 epoll_wait

man 2 epoll_wait

 #include <sys/epoll.h>

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

功能:等待事件发生 

  • epfd — 这是 epoll_create 成功后所返回的文件描述符,表征一个 epoll 实例(监视工具)
  • events — 指向结构体数组,当某个任务所监视的 fd 及其关注的事件发生后,函数返回并将监视结果与该任务有关的一些信息回填至该数组。因为可能有多个任务监视到事件的发生,所以我们才用数组收集结果,数组空间由调用者负责申请
  • maxevents — 指定 events 所指向的数组大小
  • timeout — 类似于 select 中的 timeout。如果没有任务监视到事件的发生,则 epoll_wait 会阻塞 timeout 毫秒。如果 timeout 设为 -1,则 epoll_wait 会一直阻塞,直到有任务监视到事件发生;如果 timeout 设为 0,则 epoll_wait 会立即返回
  • 返回值表示 events 中存储的元素个数,表示有多少个任务监视到了事件的发生。最大不超过 maxevents

3.3.4 代码示例

代码示例:重构 3.1 中的代码。要求用 epoll 替代 select

代码实现如下,详见注释

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

#define TTY1 "/dev/tty2"
#define TTY2 "/dev/tty3"

#define BUFSIZE 1024

enum
{
	STATE_R = 1,    // 读取态
	STATE_W,        // 写入态
	STATE_Ex,       // 异常error态
	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->state = STATE_Ex;  // 真错,转移至异常态
					fsm->errstr = "error in read()";
				}
			}
			else {
				fsm->state = STATE_W;   // 读取成功,转移至写态
				fsm->pos = 0;   // 此时buf中所有内容 都还没被写入,pos置0
			}
			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->state = STATE_Ex;  // 真错,转移至异常态
					fsm->errstr = "error in write()";
				}
			}
			else
			{
				fsm->pos += ret;        // 写入了ret 字节
				fsm->len -= ret;        // 尚未被写入内容的起始位置后移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 sth */
			break;
		default:
			abort();
			break;
	}
}

static void relay(int fd1, int fd2) {

	// 将文件描述符fd1与fd2都设置为“以非阻塞形式打开”
	int fd1_save = fcntl(fd1, F_GETFL);
	fcntl(fd1, F_SETFL, fd1_save|O_NONBLOCK);
	int fd2_save = fcntl(fd2, F_GETFL);
	fcntl(fd2, F_SETFL, fd2_save|O_NONBLOCK);

	struct fsm_st fsm12, fsm21;     // 两个任务
					// 需要两个状态转移过程
					// fsm12维护从fd1读写入fd2的 状态转移过程
					// fsm21维护从fd2读写入fd1的 状态转移过程
	fsm12.state = STATE_R;
	fsm12.sfd = fd1;
	fsm12.dfd = fd2;
	fsm21.state = STATE_R;
	fsm21.sfd = fd2;
	fsm21.dfd = fd1;        // 初始状态


	struct epoll_event ev;
	int epfd = epoll_create(10);    // 创建epoll实例,参数大于0即可
	if (epfd < 0){
		perror("epoll_create()");
		exit(1);
	}

	epoll_ctl(epfd, EPOLL_CTL_ADD, fd1, &ev);       // 注册监视任务:使epoll实例开始监视fd1
	epoll_ctl(epfd, EPOLL_CTL_ADD, fd2, &ev);       // 注册监视任务:使epoll实例开始监视fd2
	while (fsm12.state != STATE_T || fsm21.state != STATE_T) {
		// 布置监视任务,我们写在了while循环外


		// fms12是从fd1读数据,写入fd2
		// fms21是从fd2读数据,写入fd1
		ev.data.fd = fd1;       // 记录与监视任务有关的一些信息,这里表示该任务监视的fd1
		ev.events = 0;
		if (fsm12.state == STATE_R)     // fsm12为可读态,监视fd1何时可读
			ev.events |= EPOLLIN;
		if (fsm21.state == STATE_W)     // fsm21为可写态,监视fd1何时可写
			ev.events |= EPOLLOUT;
		epoll_ctl(epfd, EPOLL_CTL_MOD, fd1, &ev);       // 指定监视fd1上的事件为ev所指定的事件

		ev.data.fd = fd2;       // 记录与监视任务有关的一些信息,这里表示该任务监视的fd2
		ev.events = 0;
		if (fsm12.state == STATE_W)     // fsm12为可写态,监视fd2何时可写
			ev.events |= EPOLLOUT;
		if (fsm21.state == STATE_R)     // fsm21为可读态,监视fd2何时可读
			ev.events |= EPOLLIN;
		epoll_ctl(epfd, EPOLL_CTL_MOD, fd2, &ev);       // 指定监视fd1上的事件为ev所指定的事件

		// 监视
		while (epoll_wait(epfd, &ev, 1, -1) < 0) {      // 通过ev获取监视到事件的任务的返回信息
			if (errno == EINTR)
				continue;
			perror("epoll_wait()");
			exit(1);
		}
		// 当执行到此处,必定说明已经有某个任务监视到了其所关注的事件,监视结果及该任务相关信息已经被回填至ev
		// 查看监视结果,ev.events存放了监视结果;ev.data存放了该任务相关信息(如该任务监视的哪个文件描述符)
		// 根据结果推动状态机
		if (ev.data.fd == fd1 && ev.events & EPOLLIN || ev.data.fd == fd2 && ev.events & EPOLLOUT || fsm12.state == 3)       // fd1可读或者fd2可写或者状态机为异常态,无条件推动fsm12
			fsm_driver(&fsm12);
		if (ev.data.fd == fd1 && ev.events & EPOLLOUT || ev.data.fd == fd2 && ev.events & EPOLLIN || fsm21.state == 3)       // fd1可写或者fd2可读或者状态机为异常态,无条件推动fsm21
			fsm_driver(&fsm21);
	}

	fcntl(fd1, F_SETFL, fd1_save);  // 恢复文件描述符的默认打开方式
	fcntl(fd2, F_SETFL, fd2_save);
	close(epfd);    // 不再需要epoll实例,关闭它
}

int main() {
	int fd1, fd2;

	fd1 = open(TTY1, O_RDWR);
	if (fd1 < 0)
	{
		perror("open()");
		exit(1);
	}
	write(fd1, "TTY1\n", 5);

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

	relay(fd1, fd2);        // 数据交换的双方为fd1和fd2

	close(fd2);
	close(fd1);

	exit(0);
}

四、其他读写函数

4.1 readv

man 2 readv 

#include <sys/uio.h>

ssize_t readv(int fd, const struct iovec *iov, int iovcnt);

功能:读文件描述符并将结果写入离散空间

  • fd — 指定读哪个文件描述符
  • iov — 指向一个用来存放 iovec 结构体的数组。每个结构体表示其中一块离散空间
  • iovcn — 用来描述数组长度,表示有多少块离散空间

其中,iovec 结构体的内容如下

struct iovec {
    void  *iov_base;    /* Starting address */
    size_t iov_len;     /* Number of bytes to transfer */
};

4.2 writev

man 2 writev 

#include <sys/uio.h>

ssize_t writev(int fd, const struct iovec *iov, int iovcnt);

功能:将离散空间的数据写入文件描述符

  • fd — 指定写哪个文件描述符
  • iov — 指向一个用来存放 iovec 结构体的数组。每个结构体表示其中一块离散空间
  • iovcn — 用来描述数组长度,表示有多少块离散空间

五、存储映射 IO

5.1 简介

mmap 的核心思想是将文件的内容直接映射到进程的内存地址空间中,让文件数据的访问更接近于直接访问内存的高效率,而无需传统的读写系统调用

man 2 mmap

#include <sys/mman.h>

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

功能:将文件的内容直接映射到进程的内存地址空间中

  • addr — 指定将文件的内容映射到哪个位置(即指定映射区域的地址)。通常设置为 NULL,表示由操作系统自动选择一个地址
  • length — 希望映射到内存的目标文件的长度。这个长度是以字节为单位的,它决定了映射区域的大小
  • prot — 决定了进程能对映射区域做哪些类型的访问,详见 man
  • flags — 控制映射类型和选项的标志,详见 man
  • fd — 指定希望映射哪个文件的内容
  • offset — 指定希望从文件的哪个位置开始映射
  • 成功返回映射区域的地址;失败返回 MAP_FAILED 并设置 errno

代码示例:通过 mmap 统计某个文件中某个字母的个数

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

int main(int argc, char ** argv) {
        if (argc < 3)
        {
                fprintf(stderr, "Usage: %s <filename> <alpha>\n", argv[0]);
                exit(1);
        }

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

        struct stat statbuf;
        if(stat(argv[1], &statbuf) < 0)
        {
                perror("state()");
                exit(1);
        }

        // 操作系统自动选择将fd映射到哪个位置,映射整个文件内容,需要映射区域的大小就是文件大小
        // 进程对映射区域可读,且对映射区域的修改会反映到被映射的文件中
        // 返回映射区域的首地址,我们用char*接收,这样一来可以像操作字符串一样操作文件内容
        char * str = mmap(NULL, statbuf.st_size, PROT_READ, MAP_SHARED, fd, 0);
        if (str == MAP_FAILED)
        {
                perror("mmap()");
                exit(1);
        }
        close(fd);    // 已经映射过来了,fd就不用了

        long int count = 0;
        for (int i = 0; i < statbuf.st_size; ++i)
        {
                if (str[i] == argv[2][0])
                        count++;
        }
        printf("%ld\n", count);

        munmap(str, statbuf.st_size);    // 撤销通过mmap创建的内存映射
        exit(0);
}

5.2 共享内存用作进程间通信

基本思路是,将同一个文件映射到两个不同进程。此时这个文件就像一个“共享内存”

我们实现一个基于共享内存进行父子进程通信的示例

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

#define MEMSIZE 1024

int main()
{
        // 让系统自己找映射区域的地址
        // 因为要通信,因此需要能对这片区域可读可写
        // 因为要通信,因此对这片区域的修改应该能够反映到被映射的文件
        // 采用匿名映射就不用去指定特定文件了,比较方便。此时传-1给fd参数即可
        char * ptr = mmap(NULL, MEMSIZE, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0);
        if (ptr == MAP_FAILED)
        {
                perror("mmap()");
                exit(1);
        }

        pid_t pid = 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);
                munmap(ptr, MEMSIZE);
                exit(0);
        }
        exit(0);
}

子进程写进共享内存的字符串成功在父进程中打印了出来,通信成功!


六、文件锁

之前介绍过多线程里面的 pthread_mutex 锁,那个锁是在单一进程内防止线程之间出现资源访问竞争

而文件锁用于同步多个进程对同一文件执行的 IO 操作,防止进程之间出现文件访问竞争

有多个函数能够实现文件锁,我们只介绍一部分 


6.1 lockf

man 3 lockf

#include <unistd.h>

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

功能:对某个文件的特定部分进行锁操作(上锁、解锁、测试是否上锁)

  • fd — 用来指定对哪个文件进行锁操作
  • len — 用来指定取文件的哪部分并对其进行锁操作。当 len > 0,则从pos 指针所指位置起取 len 字节;当 len < 0,则取 pos 所指位置之前的 len 字节;当 len = 0,则从 pos 所指位置起取到文件末尾(哪怕文件末尾位置一直在不断变化)
  • cmd — 决定具体对指定文件的指定部分做什么样的锁操作。其值及含义如下
含义
F_LOCK上锁。若该部分已被其他进程上锁,则阻塞直到被解锁
F_TLOCK尝试上锁。若该部分已被其他上锁,不阻塞并返回一个错误
F_ULOCK对某部分解锁
F_TEST测试某部分是否上锁。未上锁返回 0;上锁返回 -1 并设置 errno 为 EAGAIN

注意:close 一个文件描述符会解开其对应文件上当前进程拥有的锁(man close)

而锁是和文件关联的,两个不同的文件描述符可能表征相同的文件,如下图所示。因此,如果此时 close 其中一个文件描述符 fd2,则其对应的文件会解锁。从 fd1 的视角看,相当于一个意外解锁:明明没有通过 fd1 调用 ulock,fd1 所对应的文件还是被解锁了


代码示例:多进程同时操作一个文件。每个进程都会:

  1. 从文件中获取值
  2. 将值加一
  3. 用加一后的值替换文件中原来的值

不上锁的版本:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <wait.h>
#include <sys/types.h>

#define PROCNUM 2000 //创建2000个进程
#define LINESIZE 1024

#define FILENAME "./tmp"

static void func_add(void) {

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

        char linebuf[LINESIZE];
        fgets(linebuf, LINESIZE, fp);   // 获取
        fseek(fp, 0, SEEK_SET); // 将文件位置指针pos定位到文件首,这样才能实现覆盖原值
        fprintf(fp, "%d\n", atoi(linebuf)+1);   // 加一后写回去
        fflush(fp);
        fclose(fp);
}

int main() {

        pid_t pid;

        for (int i = 0; i < PROCNUM; ++i) {     // 父进程不断创建子进程
                pid = fork();
                if (pid < 0)    // error
                {
                        perror("fork()");
                        exit(1);
                }
                else if (pid == 0)      // child do sth
                {
                        func_add();     // do sth
                        exit(0);
                }
                else    // parent continue to fork
                {
                        continue;
                }
        }

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

        exit(0);

}

按理来说,2000 个进程每个都要取值加一,应该每次都会增加 2000 的,为什么结果不一致?

答案还是因为多进程竞争

因此我们需要用到文件锁,保证多个进程不能同时操作单个文件

上锁版本如下:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <wait.h>
#include <sys/types.h>

#define PROCNUM 2000    //创建2000个进程
#define LINESIZE 1024

#define FILENAME "./tmp"

static void func_add(void) {

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

        char linebuf[LINESIZE];

        int fd = fileno(fp);    // 我们对文件上锁需要用到文件描述符fd,需要从FILE结构体中找到fd
        if (lockf(fd, F_LOCK, 0) < 0)
        {
                perror("lockf()");
                exit(1);
        }

        fgets(linebuf, LINESIZE, fp);   // 获取
        fseek(fp, 0, SEEK_SET); // 将文件位置指针pos定位到文件首,这样才能实现覆盖原值
        fprintf(fp, "%d\n", atoi(linebuf)+1);   // 加一后写回去
        fflush(fp);

        lockf(fd, F_ULOCK, 0);  // 解锁

        fclose(fp);
}

int main() {

        pid_t pid;

        for (int i = 0; i < PROCNUM; ++i) {     // 父进程不断创建子进程
                pid = fork();
                if (pid < 0)    // error
                {
                        perror("fork()");
                        exit(1);
                }
                else if (pid == 0)      // child do sth
                {
                        func_add();     // do sth
                        exit(0);
                }
                else    // parent continue to fork
                {
                        continue;
                }
        }

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

        exit(0);

}

我们可以看到,2000 个进程每个都要取值加一,因此每次都会增加 2000,成功!

6.2 flock

功能类似 lockf,也能实现文件锁操作,略

6.3 fcntl

也能实现文件锁操作,略 



为什么更新速度慢了?因为导师得知本人找到工作后又开始安排项目了/(ㄒoㄒ)/~~ 

等等,为什么研三还在做项目!!!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/140110.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

python3+requests+unittest实战系列【一】

1.环境准备 python3 pycharm编辑器 2.框架目录展示 &#xff08;该套代码只是简单入门&#xff0c;有兴趣的可以不断后期完善&#xff09; &#xff08;1&#xff09;run.py主运行文件&#xff0c;运行之后可以生成相应的测试报告&#xff0c;并以邮件形式发送&#xff1b;…

postswigger 靶场(CSRF)攻略-- 1.没有防御措施的 CSRF 漏洞

靶场地址&#xff1a; What is CSRF (Cross-site request forgery)? Tutorial & Examples | Web Security Academy (portswigger.net)https://portswigger.net/web-security/csrf 没有防御措施的 CSRF 漏洞 题目中已告知易受攻击的是电子邮件的更改功能&#xff0c;而目…

Python实现WOA智能鲸鱼优化算法优化BP神经网络回归模型(BP神经网络回归算法)项目实战

说明&#xff1a;这是一个机器学习实战项目&#xff08;附带数据代码文档视频讲解&#xff09;&#xff0c;如需数据代码文档视频讲解可以直接到文章最后获取。 1.项目背景 鲸鱼优化算法 (whale optimization algorithm,WOA)是 2016 年由澳大利亚格里菲斯大学的Mirjalili 等提…

DeepFool: a simple and accurate method to fool deep neural networks

DeepFool: a simple and accurate method to fool deep neural networks----《DeepFool&#xff1a;一种简单而准确的欺骗深度神经网络的方法》 摘要 最先进的深度神经网络已经在许多图像分类任务上取得了令人印象深刻的结果。然而&#xff0c;这些相同的架构已被证明对于图像…

纯c语言模拟栈(初学必看)

1.栈的概念及其结构 栈是一种特殊的线性表&#xff0c;在栈这个结构里&#xff0c;越先存进去的数据越难取出来。 这个结构就像是一个只有一端有打开的容器&#xff0c;越先放进去的球越在底部&#xff0c;想要把底部的球拿出来&#xff0c;就必须先把前面的求拿出来。像这种”…

使用 ESP-IDF-SBOM 生成软件物料清单

概述 “软件物料清单” (SBOM) 已经成为软件安全和软件供应链风险管理的关键组成部分。SBOM 是与应用程序相关的所有软件组件、依赖项和元数据的详尽清单。 乐鑫认为&#xff0c;SBOM 信息是确保联网设备安全性的关键。因此&#xff0c;我们现在提供了相关工具和解决方案&…

02-瑞吉外卖员工表的增删改查

添加员工信息 执行流程 第一步: 用户点击添加员工按钮跳转到add.html页面,然后在页面中输入要添加的员工的信息 第二步: 用户点击保存按钮发送Ajax请求将用户输入的员工信息以json的格式提交到服务端 第三步: 服务端Controller接收页面提交的json格式的数据并转化为java对象…

Vue3 ref函数和active函数

一、ref函数 我们在setup函数中导出的属性和方法虽然能够在模板上展示出来&#xff0c;但是并没有给属性添加响应式&#xff0c;因此&#xff0c;我们需要使用ref函数来为我们的数据提供响应式。 &#xff08;一&#xff09;引入ref函数 import { ref } from "vue"…

Sprint Boot 学习路线 6

测试 Spring提供了一组测试工具&#xff0c;可以轻松地测试Spring应用程序的各个组件&#xff0c;包括控制器、服务、存储库和其他组件。它具有丰富的测试注释、实用程序类和其他功能&#xff0c;以帮助进行单元测试、集成测试等。 JPA测试 Spring JPA&#xff08;Java Pers…

pytorch框架学习(tensorboard的使用)

什么是tensorboard&#xff1f; tensorboard是一个可视化工具&#xff0c;它可以把训练过程中的数据变化以图像的形式绘制出来&#xff0c;或者记录训练过程中使用的图片 tensorboard的安装&#xff1a; 在pycharm的终端中输出安装命令后自动安装—— pip install tensorbo…

加速mvn下载seatunnel相关jar包

seatunnel安装的时候&#xff0c;居然要使用mvnw来下载jar包&#xff0c;而且是从https://repo.maven.apache.org 下载&#xff0c;速度及其缓慢&#xff0c;改用自己本地的mvn下载。 修改其安装插件相关脚本&#xff0c;复制install-plugin.sh重命名为install-plugin-mvn.sh …

玩转硬件之Micro:bit的玩法(五)——垃圾分类

垃圾分类&#xff0c;为了美好的明天 垃圾是我们生活中不可避免的产物&#xff0c;每天都有大量的垃圾被丢弃&#xff0c;如果不加以处理&#xff0c;就会给环境和人类带来严重的危害。 垃圾分类是一种有效的垃圾管理方式&#xff0c;它是指按照一定的标准或规则&#xff0c;将…

[量子计算与量子信息] 2.1 线性代数

2.1 线性代数 符号对照表 量子力学中&#xff0c;向量使用 ∣ ψ ⟩ \ket \psi ∣ψ⟩ (ket)来表示&#xff0c;可以理解为一个列向量。其对偶向量为 ⟨ ψ ∣ \bra \psi ⟨ψ∣ &#xff0c;可以理解为行向量。 向量空间中零向量直接用 0 0 0 表示&#xff0c; ∣ 0 ⟩ \…

网络安全黑客技术自学

前言 一、什么是网络安全 网络安全可以基于攻击和防御视角来分类&#xff0c;我们经常听到的 “红队”、“渗透测试” 等就是研究攻击技术&#xff0c;而“蓝队”、“安全运营”、“安全运维”则研究防御技术。 无论网络、Web、移动、桌面、云等哪个领域&#xff0c;都有攻与防…

如何利用黑群晖虚拟机和内网穿透实现公网远程访问

文章目录 前言本教程解决的问题是&#xff1a;按照本教程方法操作后&#xff0c;达到的效果是前排提醒&#xff1a; 1. 搭建群晖虚拟机1.1 下载黑群晖文件vmvare虚拟机安装包1.2 安装VMware虚拟机&#xff1a;1.3 解压黑群晖虚拟机文件1.4 虚拟机初始化1.5 没有搜索到黑群晖的解…

基于51单片机DS18B20温度检测报警系统串口设置阀值-仿真及源程序

一、系统方案 1、本设计采用51单片机作为主控器。 2、DS18B20采集温度值送到数码管显示。 3、按键报警阀值或串口设置阀值。 4、测量温度小于下限或大于上限&#xff0c;蜂鸣器报警。 二、硬件设计 原理图如下&#xff1a; 三、单片机软件设计 1、首先是系统初始化 uint z; …

八皇后问题

1.八皇后BOSS 2.战术分析 第一个皇后先放第一行第一列第二个皇后放在第二行第一列、然后判断是否OK,如果不OK,继续放在第二列、第三列、依次把所有列都放完,找到一个合适继续第三个皇后,还是第一列、第二列.…直到第8个皇后也能放在一个不冲突的位置,算是找到了一个正确解当得到…

LeetCode(9)跳跃游戏【数组/字符串】【中等】

目录 1.题目2.答案3.提交结果截图 链接&#xff1a; 55. 跳跃游戏 1.题目 给你一个非负整数数组 nums &#xff0c;你最初位于数组的 第一个下标 。数组中的每个元素代表你在该位置可以跳跃的最大长度。 判断你是否能够到达最后一个下标&#xff0c;如果可以&#xff0c;返回…

青少年编程学习 等级考试 蓝桥杯/NOC/GESP等比赛资料合集

一、博主愚见 在当今信息技术高速发展的时代&#xff0c;编程已经成为了一种必备的技能。随着社会对于科技人才的需求不断增加&#xff0c;青少年编程学习正逐渐成为一种趋势。为了更好地帮助青少年学习编程&#xff0c;提升他们的技能和素质&#xff0c;博主结合自身多年从事青…

五款常见的自动化测试框架

在自动化的软件测试系统实现过程中使用框架设计可以使得测试脚本的维护量减至最少。然而&#xff0c;大量的自动化测试工具均采用传统的“录制一回放”模 型&#xff0c;导致了较高的脚本维护量&#xff0c;因为测试数据在测试脚本程序中是以硬编码方式实现的。此外&#xff0…