Linux系统编程第五部分:高级IO

主要内容:

1、非阻塞IO(补充:有限状态机编程)

2、IO多路转接(IO多路复用)

3、其他读写函数

4、存储映射IO

5、文件锁

1、非阻塞IO

我们之前写的程序都是阻塞IO,但是在有些情况下,阻塞IO就显得没那么优秀。比如我们来看下一个简单的数据中继:

如果是阻塞IO,并且是一个任务(一个进程或者线程)去用上图1来进行,那么要是A数据空了,而B数据溢出,就会阻塞,后面的也无法进行。

而如果是分为两个任务,则可以用上图2,一个负责读A写B,一个负责读B写A。

或者,使用非阻塞IO,用一个任务也可以让上图1不会卡住,因为如果读A不行就转去读B就是,也可以正常推进。

有限状态机编程:

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

简单流程:一个程序的自然流程是结构化的。

复杂流程:一个程序的自然流程不是结构化的。

自然流程:作为一个人类的思维,解决一个问题最直观的思路,比如把一个大象放进冰箱的步骤。

这样说可能不是很好理解,我们用有限状态机编程来实现一下中继就好理解了。

说到底,其实这个读写不就是我们在第一章节IO里实现的mycpy吗,我们再次把这个程序来进行一下重构:

先来看看有限状态机在这里面的编程思路:

实现:

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

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

// 状态
enum
{
    STATE_R = 1,
    STATE_W,
    STATE_Ex,
    STATE_T
};

// 状态机
struct fsm_st
{
    int state;         // 状态
    int sfd;           // 源
    int dfd;           // 目标
    char buf[BUFSIZE]; // 读写缓冲
    int pos;           // 文件读取位置
    int len;           // 读到的数量
    char *msg;         // 信息
};

// 状态机推动
static void fsm_driver(struct fsm_st *fsm)
{
    int writelen;
    switch (fsm->state)
    {
    case STATE_R:
        fsm->len = read(fsm->sfd, fsm->buf, BUFSIZE);
        if (fsm->len < 0) // 出错,判断假错还是真错
        {
            if (errno == EAGAIN) // 假错:暂时没内容读到,那就继续读
            {
                fsm->state = STATE_R;
            }
            else // 真错,变为异常态
            {
                fsm->msg = "read()";
                fsm->state = STATE_Ex;
            }
        }
        else if (fsm->len == 0) // 读完,变为终止态
        {
            fsm->state = STATE_T;
        }
        else // 变为写态
        {
            fsm->pos = 0; // 位置清零
            fsm->state = STATE_W;
        }
        break;
    case STATE_W:
        writelen = write(fsm->dfd, fsm->buf + fsm->pos, fsm->len);
        // 写够没有(读的个数是否等于写的个数)
        if (writelen < 0) // 出错,看真错还是假错
        {
            if (errno == EAGAIN) // 假错,继续写
            {
                fsm->state = STATE_W;
            }
            else // 真错,去异常态
            {
                fsm->msg = "write()";
                fsm->state = STATE_Ex;
            }
        }
        else
        {
            fsm->pos += writelen; // 记录当前写到的位置,要是没写够,下次从当前位置接着写
            fsm->len -= writelen;
            if (fsm->len == 0) // 读多少就成功的写了多少,那就回到读态继续
            {
                fsm->state = STATE_R;
            }
            else // 写的个数小于读的个数,从当前位置继续去写
            {
                fsm->state = STATE_W;
            }
        }
        break;
    case STATE_Ex:
        // 异常态就报报错就可以前往终止态了
        perror(fsm->msg);
        fsm->state = STATE_T;
        break;
    case STATE_T:
        /*do sth*/
        break;
    default:
        /*do sth*/
        break;
    }
}

static void relay(int fd1, int fd2)
{
    int fd1_save, fd2_save;
    struct fsm_st fsmab, fsmba;

    // 首先把两个文件加上非阻塞性质
    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);
    // 状态机赋值
    fsmab.state = STATE_R;
    fsmab.sfd = fd1;
    fsmab.dfd = fd2;

    fsmba.state = STATE_R;
    fsmba.sfd = fd2;
    fsmba.dfd = fd1;
    // 开始推动状态机运行
    while (fsmab.state != STATE_T || fsmba.state != STATE_T)
    {
        fsm_driver(&fsmab);
        fsm_driver(&fsmba);
    }
    // 注意,退出这个模块的时候,应该把状态恢复到进入这个模块之前的状态
    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 (fd2 < 0)
    {
        perror("open()");
        exit(1);
    }
    // 中继
    relay(fd1, fd2);

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

现在使用tty11(ctrl+alt+f11)和tty12(ctrl+alt+f12)就可以互相发送接收消息了。

下面我们自己封装一个,用于多对任务的推进,比如两对任务同时读写,或者三对。。。

relayer.h

#ifndef RELAYER_H__
#define RELAYER_H__

#define JOBMAX 10000

// 任务的状态
enum
{
    STATE_RUNNING = 1,
    STATE_CANCELED,
    STATE_OVER
};

/**
 *添加任务
 return >= 0 成功,返回当前任务ID
        == -EINVAL 失败,参数非法
        == -ENOSPC 失败,任务数组满
        == -ENOMEM 失败,内存分派有误
*/
int rel_addjob(int fd1, int fd2);

/**
 * 卸载任务我就不实现了,大家可以自己实现module_unload
 * 回收资源等等
 */

#endif

relayer.c

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

#include "relayer.h"

#define BUFSIZE 1024

pthread_mutex_t mut = PTHREAD_MUTEX_INITIALIZER;

pthread_once_t rel_once = PTHREAD_ONCE_INIT;

static struct rel_job_st *job[JOBMAX];

// 状态机的状态
enum
{
    STATE_R = 1,
    STATE_W,
    STATE_Ex,
    STATE_T
};

// 状态机
struct rel_fsm_st
{
    int state;         // 状态
    int sfd;           // 源
    int dfd;           // 目标
    char buf[BUFSIZE]; // 读写缓冲
    int pos;           // 文件读取位置
    int len;           // 读到的数量
    char *msg;         // 信息
};

// 任务
struct rel_job_st
{
    int job_state;
    int fd1;
    int fd2;
    struct rel_fsm_st fsm12, fsm21;
    int fd1_save, fd2_save;
    // 还可以有些其他,比如任务开始时间等等,大家自行可以拓展
};

// 状态机推动
static void job_fsm_driver(struct rel_fsm_st *fsm)
{
    int writelen;
    switch (fsm->state)
    {
    case STATE_R:
        fsm->len = read(fsm->sfd, fsm->buf, BUFSIZE);
        if (fsm->len < 0) // 出错,判断假错还是真错
        {
            if (errno == EAGAIN) // 假错:暂时没内容读到,那就继续读
            {
                fsm->state = STATE_R;
            }
            else // 真错,变为异常态
            {
                fsm->msg = "read()";
                fsm->state = STATE_Ex;
            }
        }
        else if (fsm->len == 0) // 读完,变为终止态
        {
            fsm->state = STATE_T;
        }
        else // 变为写态
        {
            fsm->pos = 0; // 位置清零
            fsm->state = STATE_W;
        }
        break;
    case STATE_W:
        writelen = write(fsm->dfd, fsm->buf + fsm->pos, fsm->len);
        // 写够没有(读的个数是否等于写的个数)
        if (writelen < 0) // 出错,看真错还是假错
        {
            if (errno == EAGAIN) // 假错,继续写
            {
                fsm->state = STATE_W;
            }
            else // 真错,去异常态
            {
                fsm->msg = "write()";
                fsm->state = STATE_Ex;
            }
        }
        else
        {
            fsm->pos += writelen; // 记录当前写到的位置,要是没写够,下次从当前位置接着写
            fsm->len -= writelen;
            if (fsm->len == 0) // 读多少就成功的写了多少,那就回到读态继续
            {
                fsm->state = STATE_R;
            }
            else // 写的个数小于读的个数,从当前位置继续去写
            {
                fsm->state = STATE_W;
            }
        }
        break;
    case STATE_Ex:
        // 异常态就报报错就可以前往终止态了
        perror(fsm->msg);
        fsm->state = STATE_T;
        break;
    case STATE_T:
        /*do sth*/
        break;
    default:
        /*do sth*/
        break;
    }
}

// 寻找数组空闲位置
static int get_free_pos()
{
    int i;
    for (i = 0; i < JOBMAX; ++i)
    {
        if (job[i] == NULL)
        {
            return i;
        }
    }
    return -1;
}

static void *rel_func(void *p)
{
    int i;
    while (1)
    {
        // 便利job数组的时候要加锁!
        pthread_mutex_lock(&mut);
        for (i = 0; i < JOBMAX; ++i)
        {
            if (job[i] != NULL)
            {
                if (job[i]->job_state == STATE_RUNNING)
                {
                    job_fsm_driver(&job[i]->fsm12);
                    job_fsm_driver(&job[i]->fsm21);
                    // 推动后判断状态
                    // 如果两个都终止了,就把任务状态写成终止态
                    if (job[i]->fsm12.state == STATE_T && job[i]->fsm21.state == STATE_T)
                    {
                        job[i]->job_state = STATE_OVER;
                    }
                }
            }
        }
        pthread_mutex_unlock(&mut);
    }
}

static void rel_module_load(void)
{
    pthread_t tid;
    int err;
    err = pthread_create(&tid, NULL, rel_func, NULL);
    if (err)
    {
        fprintf(stderr, "pthread_create():%s\n", strerror(err));
        exit(1);
    }
}

int rel_addjob(int fd1, int fd2)
{
    int pos;
    struct rel_job_st *me;
    if (fd1 < 0 || fd2 < 0)
    {
        return -EINVAL;
    }
    // 开始推动任务
    pthread_once(&rel_once, rel_module_load);
    me = malloc(sizeof(*me));
    if (me == NULL)
    {
        return -ENOMEM;
    }
    // 赋值
    me->fd1 = fd1;
    me->fd2 = fd2;
    me->job_state = STATE_RUNNING;
    // 把文件变为非阻塞
    me->fd1_save = fcntl(me->fd1, F_GETFL);
    fcntl(fd1, F_SETFL, me->fd1_save | O_NONBLOCK);
    me->fd2_save = fcntl(me->fd2, F_GETFL);
    fcntl(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(&mut);
    pos = get_free_pos();
    if (pos < 0)
    {
        // 记住退出之前要解锁!恢复文件之前的属性!free刚刚malloc的me!
        pthread_mutex_unlock(&mut);
        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(&mut);
    return pos;
}

main.c

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

#define TTY1 "/dev/tty11"
#define TTY2 "/dev/tty12"
#define TTY3 "/dev/tty10"
#define TTY4 "/dev/tty9"

int main()
{
    int job1, job2;
    int fd1, fd2, fd3, fd4;
    fd1 = open(TTY1, O_RDWR);
    if (fd1 < 0)
    {
        perror("open()");
        exit(1);
    }
    fd2 = open(TTY2, O_RDWR | O_NONBLOCK);
    if (fd2 < 0)
    {
        perror("open()");
        exit(1);
    }
    fd3 = open(TTY3, O_RDWR);
    if (fd3 < 0)
    {
        perror("open()");
        exit(1);
    }
    fd4 = open(TTY4, O_RDWR);
    if (fd4 < 0)
    {
        perror("open()");
        exit(1);
    }
    // 添加任务fd1 fd2
    job1 = rel_addjob(fd1, fd2);
    if (job1 < 0)
    {
        fprintf(stderr, "rel_addjob():%s\n", strerror(-job1));
        exit(1);
    }
    // 添加任务fd3 fd4
    job2 = rel_addjob(fd3, fd4);
    if (job2 < 0)
    {
        fprintf(stderr, "rel_addjob():%s\n", strerror(-job2));
        exit(1);
    }

    // 别让它结束
    while (1)
    {
        pause();
    }

    // 关闭(虽然执行不到这些)
    close(fd2);
    close(fd1);
    close(fd3);
    close(fd4);
    exit(0);
}

现在使用tty11(ctrl+alt+f11)和tty12(ctrl+alt+f12)可以互相发送接收消息。并且tty9(ctrl+alt+f9)和tty10(ctrl+alt+f10)也可以相互发送接收消息,并且互不干扰。

我们可以发现,不管是上面两个的哪个程序,我们在运行的时候它始终处于一个忙等的状态,cpu占用率拉满,因为加入说没东西,那read()那里返回的就是一个假错,就会一直循环判断。这个时候我们就可以用IO多路转接/IO多路复用。

2、IO多路转接(IO多路复用)

它的作用说白了就是监视文件描述符的行为的。当当前文件描述符发生了我感兴趣的行为的时候,才去做后续操作

主要函数:

select()、poll()、epoll()

#include <sys/select.h>

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
//nfds:监视的文件描述符的最大的那个再加一
//readfds:所关心的文件描述符中可读的
//writefds:所关心的文件描述符中可写的
//exceptfds:所关心的文件描述符中异常的
//timeout:超时设置,如果不设置,就是忙等,知道感兴趣的事情发送为止,比如readfds中有可以读的文件描述符了,select才返回
//返回值:成功返回的是当前发生了你感兴趣的文件描述符的个数;失败返回-1并设置errno,超时返回假错EINTR

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


//其实这里fd_set集合的操作和我们之前的sigset_t的操作差不多
void FD_CLR(int fd, fd_set *set);//从集合中删除fd
int  FD_ISSET(int fd, fd_set *set);//看一个fd是否在集合中
void FD_SET(int fd, fd_set *set);//把fd加入集合
void FD_ZERO(fd_set *set);//清空集合

现在我们用select把之前的两个设备互发信息的程序从忙等改变为非忙等,先来看看之前的忙等在哪里:

// 开始推动状态机运行
while (fsmab.state != STATE_T || fsmba.state != STATE_T)
{
      fsm_driver(&fsmab);
      fsm_driver(&fsmba);
}

不停的调用while来推状态机。

现在我们加入select来改写,不盲目地推,直到发生了感兴趣的动作为止。先来搭个框架,看看我们应该是个什么逻辑来写:

    // 开始推动状态机运行
    while (fsmab.state != STATE_T || fsmba.state != STATE_T)
    {
        // 布置监视任务

        // 监视
        select();

        // 查看监视结果

        if ()
        {
            fsm_driver(&fsmab);
        }
        if ()
        {
            fsm_driver(&fsmba);
        }
    }

好,现在我们来补全程序:

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

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

// 状态
enum
{
    STATE_R = 1,
    STATE_W,
    STATE_Ex,
    STATE_T
};

// 状态机
struct fsm_st
{
    int state;         // 状态
    int sfd;           // 源
    int dfd;           // 目标
    char buf[BUFSIZE]; // 读写缓冲
    int pos;           // 文件读取位置
    int len;           // 读到的数量
    char *msg;         // 信息
};

// 状态机推动
static void fsm_driver(struct fsm_st *fsm)
{
    int writelen;
    switch (fsm->state)
    {
    case STATE_R:
        fsm->len = read(fsm->sfd, fsm->buf, BUFSIZE);
        if (fsm->len < 0) // 出错,判断假错还是真错
        {
            if (errno == EAGAIN) // 假错:暂时没内容读到,那就继续读
            {
                fsm->state = STATE_R;
            }
            else // 真错,变为异常态
            {
                fsm->msg = "read()";
                fsm->state = STATE_Ex;
            }
        }
        else if (fsm->len == 0) // 读完,变为终止态
        {
            fsm->state = STATE_T;
        }
        else // 变为写态
        {
            fsm->pos = 0; // 位置清零
            fsm->state = STATE_W;
        }
        break;
    case STATE_W:
        writelen = write(fsm->dfd, fsm->buf + fsm->pos, fsm->len);
        // 写够没有(读的个数是否等于写的个数)
        if (writelen < 0) // 出错,看真错还是假错
        {
            if (errno == EAGAIN) // 假错,继续写
            {
                fsm->state = STATE_W;
            }
            else // 真错,去异常态
            {
                fsm->msg = "write()";
                fsm->state = STATE_Ex;
            }
        }
        else
        {
            fsm->pos += writelen; // 记录当前写到的位置,要是没写够,下次从当前位置接着写
            fsm->len -= writelen;
            if (fsm->len == 0) // 读多少就成功的写了多少,那就回到读态继续
            {
                fsm->state = STATE_R;
            }
            else // 写的个数小于读的个数,从当前位置继续去写
            {
                fsm->state = STATE_W;
            }
        }
        break;
    case STATE_Ex:
        // 异常态就报报错就可以前往终止态了
        perror(fsm->msg);
        fsm->state = STATE_T;
        break;
    case STATE_T:
        /*do sth*/
        break;
    default:
        /*do sth*/
        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 fsmab, fsmba;
    fd_set read_set, write_set;
    int res;

    // 首先把两个文件加上非阻塞性质
    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);
    // 状态机赋值
    fsmab.state = STATE_R;
    fsmab.sfd = fd1;
    fsmab.dfd = fd2;

    fsmba.state = STATE_R;
    fsmba.sfd = fd2;
    fsmba.dfd = fd1;
    // 开始推动状态机运行
    while (fsmab.state != STATE_T || fsmba.state != STATE_T)
    {
        // 布置监视任务
        // 清空集合
        FD_ZERO(&read_set);
        FD_ZERO(&write_set);
        // 加入集合
        if (fsmab.state == STATE_R)
        {
            FD_SET(fsmab.sfd, &read_set);
        }
        if (fsmab.state == STATE_W)
        {
            FD_SET(fsmab.dfd, &write_set);
        }
        if (fsmba.state == STATE_R)
        {
            FD_SET(fsmba.sfd, &read_set);
        }
        if (fsmba.state == STATE_W)
        {
            FD_SET(fsmba.dfd, &write_set);
        }
        // 监视
        res = select(max(fd1, fd2) + 1, &read_set, &write_set, NULL, NULL);
        if (res < 0)
        {
            if (errno == EINTR) // 如果是假错
            {
                continue;
            }
            // 如果是真错
            perror("select()");
            exit(1);
        }
        // 查看监视结果
        if (FD_ISSET(fsmab.sfd, &read_set) || FD_ISSET(fsmab.dfd, &write_set))
        {
            fsm_driver(&fsmab);
        }
        if (FD_ISSET(fsmba.sfd, &read_set) || FD_ISSET(fsmba.dfd, &write_set))
        {
            fsm_driver(&fsmba);
        }
    }
    // 注意,退出这个模块的时候,应该把状态恢复到进入这个模块之前的状态
    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 (fd2 < 0)
    {
        perror("open()");
        exit(1);
    }
    // 中继
    relay(fd1, fd2);

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

运行看一下CPU:

对比一下之前的:

select其实最大的缺陷就是它的监视现场和监视结果放在同一片空间的,比如现在read集合10fd,write里10个,except里10个,然后read等到了一个,select返回1,这个时候你的write和except里的10个也随之被清空了。。。。这就很难受,这也是上面程序中为什么我们的continue要跳大的while,然后重新设置监视任务,这里只有两个还好,想想之前我们写的那个四个设备两个之间传消息的程序,最多可以有10000个任务,那每次重新设置10000个监视任务就更难受了。其次效率问题:每次调用 select 都需要将所有的文件描述符从用户空间复制到内核空间,这可能会导致性能问题,特别是当需要监视大量的文件描述符时。select 的效率可能不如其他的多路 I/O 复用技术,如 epoll。并且,当需要监视的文件描述符数量增加时,select 的性能可能会下降,因为它需要遍历整个fd_set

接下来我们介绍第二种--poll

poll是以文件描述符来组织事件的,即:在文件描述符的基础上来等待一些事件的;而select刚好相反,select是以事件为单位来组织文件描述符的。

#include <poll.h>

int poll(struct pollfd *fds, nfds_t nfds, int timeout);
//fds:结构体数组
//nfds:文件描述符个数
//timeout:超时设置  -1:阻塞  0:非阻塞  >0(毫秒为单位)超时时间

struct pollfd {
               int   fd;         /* file descriptor */
               short events;     /* requested events */
               short revents;    /* returned events */
              };

接下来我们用poll来代替刚刚的select:

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

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

struct pollfd pollfds[2];

// 状态
enum
{
    STATE_R = 1,
    STATE_W,
    STATE_Ex,
    STATE_T
};

// 状态机
struct fsm_st
{
    int state;         // 状态
    int sfd;           // 源
    int dfd;           // 目标
    char buf[BUFSIZE]; // 读写缓冲
    int pos;           // 文件读取位置
    int len;           // 读到的数量
    char *msg;         // 信息
};

// 状态机推动
static void fsm_driver(struct fsm_st *fsm)
{
    int writelen;
    switch (fsm->state)
    {
    case STATE_R:
        fsm->len = read(fsm->sfd, fsm->buf, BUFSIZE);
        if (fsm->len < 0) // 出错,判断假错还是真错
        {
            if (errno == EAGAIN) // 假错:暂时没内容读到,那就继续读
            {
                fsm->state = STATE_R;
            }
            else // 真错,变为异常态
            {
                fsm->msg = "read()";
                fsm->state = STATE_Ex;
            }
        }
        else if (fsm->len == 0) // 读完,变为终止态
        {
            fsm->state = STATE_T;
        }
        else // 变为写态
        {
            fsm->pos = 0; // 位置清零
            fsm->state = STATE_W;
        }
        break;
    case STATE_W:
        writelen = write(fsm->dfd, fsm->buf + fsm->pos, fsm->len);
        // 写够没有(读的个数是否等于写的个数)
        if (writelen < 0) // 出错,看真错还是假错
        {
            if (errno == EAGAIN) // 假错,继续写
            {
                fsm->state = STATE_W;
            }
            else // 真错,去异常态
            {
                fsm->msg = "write()";
                fsm->state = STATE_Ex;
            }
        }
        else
        {
            fsm->pos += writelen; // 记录当前写到的位置,要是没写够,下次从当前位置接着写
            fsm->len -= writelen;
            if (fsm->len == 0) // 读多少就成功的写了多少,那就回到读态继续
            {
                fsm->state = STATE_R;
            }
            else // 写的个数小于读的个数,从当前位置继续去写
            {
                fsm->state = STATE_W;
            }
        }
        break;
    case STATE_Ex:
        // 异常态就报报错就可以前往终止态了
        perror(fsm->msg);
        fsm->state = STATE_T;
        break;
    case STATE_T:
        /*do sth*/
        break;
    default:
        /*do sth*/
        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 fsmab, fsmba;
    fd_set read_set, write_set;

    // 首先把两个文件加上非阻塞性质
    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);
    // 状态机赋值
    fsmab.state = STATE_R;
    fsmab.sfd = fd1;
    fsmab.dfd = fd2;

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

    // pollfds赋值,这里不用每次循环赋值,因此放外面即可
    pollfds[0].fd = fd1;
    pollfds[1].fd = fd2;
    // 开始推动状态机运行
    while (fsmab.state != STATE_T || fsmba.state != STATE_T)
    {
        // 布置监视任务
        // 清空fd1事件
        pollfds[0].events = 0;
        if (fsmab.state == STATE_R)
        {
            pollfds[0].events |= POLLIN;
        }
        if (fsmba.state == STATE_W)
        {
            pollfds[0].events |= POLLOUT;
        }
        // 清空fd2事件
        pollfds[1].events = 0;
        if (fsmab.state == STATE_W)
        {
            pollfds[1].events |= POLLOUT;
        }
        if (fsmba.state == STATE_R)
        {
            pollfds[1].events |= POLLIN;
        }
        // 监视
        // 因为poll中,select监视结果存放位置是单独的,不会覆盖原来的
        // 因此可以直接将这里写为while,continue就可以不用跳到最外层的while了,这也克服了select的缺点之一
        while (poll(pollfds, 2, -1) < 0)
        {
            if (errno == EINTR) // 如果是假错
            {
                continue;
            }
            // 如果是真错
            perror("poll()");
            exit(1);
        }
        // 查看监视结果
        // 因为结果是位图,因此用按位与的方式去查看结果
        // 如果a可读或者b可写,即可推状态机ab
        if (pollfds[0].revents & POLLIN || pollfds[1].revents & POLLOUT)
        {
            fsm_driver(&fsmab);
        }
        // 如果b可读或者a可写,即可推状态机ba
        if (pollfds[1].revents & POLLIN || pollfds[0].revents & POLLOUT)
        {
            fsm_driver(&fsmba);
        }
    }
    // 注意,退出这个模块的时候,应该把状态恢复到进入这个模块之前的状态
    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 (fd2 < 0)
    {
        perror("open()");
        exit(1);
    }
    // 中继
    relay(fd1, fd2);

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

总之,poll各方面要比select好得多,因此我们使用poll比使用select更多些。

接下来我们介绍最后一种--epoll epoll其实是对poll加了个封装,让我们不能直接操作pollfds数组,并且epoll有个特色就是它有个union共用体,可以记录每个fd的操作。但是epoll不可移植,因为他是linux的方言

#include <sys/epoll.h>
//创建指epoll实例
int epoll_create(int size);
int epoll_create1(int flags);
//Since Linux 2.6.8, the size argument is ignored, but must be greater than zero;

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
//epfd:epoll实例
//op:操作
//fd:文件描述符
//event:操作的事件

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 */
                   };

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);//取事件
//epfd:epoll实例
//events:事件
//maxevents:因为一次可以取多个事件出来,所以可以设置数量上限。就会取maxevents个事件出来,放进events空间中去
//timeout:超时设置(单位毫秒),0表示非阻塞,-1表示阻塞

同样的,我们使用epoll来重写上面的程序:

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

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

struct epoll_event ev;

// 状态
enum
{
    STATE_R = 1,
    STATE_W,
    STATE_Ex,
    STATE_T
};

// 状态机
struct fsm_st
{
    int state;         // 状态
    int sfd;           // 源
    int dfd;           // 目标
    char buf[BUFSIZE]; // 读写缓冲
    int pos;           // 文件读取位置
    int len;           // 读到的数量
    char *msg;         // 信息
};

// 状态机推动
static void fsm_driver(struct fsm_st *fsm)
{
    int writelen;
    switch (fsm->state)
    {
    case STATE_R:
        fsm->len = read(fsm->sfd, fsm->buf, BUFSIZE);
        if (fsm->len < 0) // 出错,判断假错还是真错
        {
            if (errno == EAGAIN) // 假错:暂时没内容读到,那就继续读
            {
                fsm->state = STATE_R;
            }
            else // 真错,变为异常态
            {
                fsm->msg = "read()";
                fsm->state = STATE_Ex;
            }
        }
        else if (fsm->len == 0) // 读完,变为终止态
        {
            fsm->state = STATE_T;
        }
        else // 变为写态
        {
            fsm->pos = 0; // 位置清零
            fsm->state = STATE_W;
        }
        break;
    case STATE_W:
        writelen = write(fsm->dfd, fsm->buf + fsm->pos, fsm->len);
        // 写够没有(读的个数是否等于写的个数)
        if (writelen < 0) // 出错,看真错还是假错
        {
            if (errno == EAGAIN) // 假错,继续写
            {
                fsm->state = STATE_W;
            }
            else // 真错,去异常态
            {
                fsm->msg = "write()";
                fsm->state = STATE_Ex;
            }
        }
        else
        {
            fsm->pos += writelen; // 记录当前写到的位置,要是没写够,下次从当前位置接着写
            fsm->len -= writelen;
            if (fsm->len == 0) // 读多少就成功的写了多少,那就回到读态继续
            {
                fsm->state = STATE_R;
            }
            else // 写的个数小于读的个数,从当前位置继续去写
            {
                fsm->state = STATE_W;
            }
        }
        break;
    case STATE_Ex:
        // 异常态就报报错就可以前往终止态了
        perror(fsm->msg);
        fsm->state = STATE_T;
        break;
    case STATE_T:
        /*do sth*/
        break;
    default:
        /*do sth*/
        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 fsmab, fsmba;
    fd_set read_set, write_set;
    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);
    // 状态机赋值
    fsmab.state = STATE_R;
    fsmab.sfd = fd1;
    fsmab.dfd = fd2;

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

    // 创建epoll实例
    epfd = epoll_create(10);
    if (epfd < 0)
    {
        perror("epoll_create()");
        exit(1);
    }

    // 给epfd实例的fd1加上ev事件
    ev.events = 0;
    ev.data.fd = fd1;
    epoll_ctl(epfd, EPOLL_CTL_ADD, fd1, &ev);
    // 给epfd实例的fd2加上ev事件
    ev.events = 0;
    ev.data.fd = fd2;
    epoll_ctl(epfd, EPOLL_CTL_ADD, fd2, &ev);

    // 开始推动状态机运行
    while (fsmab.state != STATE_T || fsmba.state != STATE_T)
    {
        // 布置监视任务
        ev.data.fd = fd1;
        ev.events = 0;
        if (fsmab.state == STATE_R)
        {
            ev.events |= EPOLLIN;
        }
        if (fsmba.state == STATE_W)
        {
            ev.events |= EPOLLOUT;
        }
        epoll_ctl(epfd, EPOLL_CTL_MOD, fd1, &ev);

        ev.data.fd = fd2;
        ev.events = 0;
        if (fsmab.state == STATE_W)
        {
            ev.events |= EPOLLOUT;
        }
        if (fsmba.state == STATE_R)
        {
            ev.events |= EPOLLIN;
        }
        epoll_ctl(epfd, EPOLL_CTL_MOD, fd2, &ev);
        // 监视
        // 因为poll中,select监视结果存放位置是单独的,不会覆盖原来的
        // 因此可以直接将这里写为while,continue就可以不用跳到最外层的while了,这也克服了select的缺点之一
        while (epoll_wait(epfd, &ev, 1, -1) < 0)
        {
            if (errno == EINTR) // 如果是假错
            {
                continue;
            }
            // 如果是真错
            perror("epoll()");
            exit(1);
        }
        // 查看监视结果
        // 因为结果是位图,因此用按位与的方式去查看结果
        // 如果a可读或者b可写,即可推状态机ab
        if ((ev.data.fd == fd1 && ev.events & EPOLLIN) || (ev.data.fd == fd2 && ev.events & EPOLLOUT))
        {
            fsm_driver(&fsmab);
        }
        // 如果b可读或者a可写,即可推状态机ba
        if ((ev.data.fd == fd2 && ev.events & EPOLLIN) || (ev.data.fd == fd1 && ev.events & EPOLLOUT))
        {
            fsm_driver(&fsmba);
        }
    }
    // 注意,退出这个模块的时候,应该把状态恢复到进入这个模块之前的状态
    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 (fd2 < 0)
    {
        perror("open()");
        exit(1);
    }
    // 中继
    relay(fd1, fd2);

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

3、其他读写函数

readv、writev

这两个函数类似于read和write 函数,但是它们允许单个系统调用读入或写出多个缓冲区。

#include <sys/uio.h>

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

//fd:要操作的那个文件描述符
//iov:指向iovec结构的一个指针
//iovcnt:iov的个数

struct iovec {
               void  *iov_base;    /* Starting address *///缓冲区首地址
               size_t iov_len;     /* Number of bytes to transfer *///缓冲区长度
             };

4、存储映射IO

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

#include <sys/mman.h>

void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);//映射
//addr:指定映射存储区的起始地址
//length:映射存储区的长度
//prot:权限
//flags:特殊要求
//fd:要映射的文件
//offset:相对于fd文件的偏移量
//一句口水话描述就是:从fd这个文件的offset偏移量开始,映射length个字节到addr这块地址中,映射过来的权限是prot,特殊要求是flags
int munmap(void *addr, size_t length);//解除映射
//addr:解除的空间的地址
//length:

下面我们写个实例来看看怎么使用,我们写一个数指定文件中a的数量:

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

int main(int argc, char **argv)
{
    int fd;
    int i;
    struct stat statres;
    char *str;
    int count = 0;
    // 参数检查
    if (argc != 2)
    {
        fprintf(stderr, "please use right argc!\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); // 第一个位置写NULL表示让它自己去找个空间,我就不人为指定了
    if (str == MAP_FAILED)
    {
        perror("mmap()");
        exit(1);
    }
    close(fd);
    for (i = 0; i < statres.st_size; ++i)
    {
        if (str[i] == 'a')
        {
            count++;
        }
    }
    printf("%d\n", count);
    munmap(str, statres.st_size);
    exit(0);
}

运行结果:

liugenyi@liugenyi-virtual-machine:~/linuxsty/advio/mmap$ ./mmap /etc/services 
517

接下来我们使用这个存储映射来实现一个父子进程通信的例子,让子进程向共享内存里写,父进程从共享内存里读:

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

#define MEMSIZE 1024

int main()
{
    char *ptr;
    pid_t pid;
    ptr = mmap(NULL, MEMSIZE, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0); // fd写-1表示不依赖任何文件描述符;MAP_ANONYMOUS匿名映射:映射区不与任何文件关联
    if (ptr == MAP_FAILED)
    {
        perror("mmap()");
        exit(1);
    }
    pid = fork();
    if (pid < 0) // 创建子进程失败
    {
        munmap(ptr, MEMSIZE);
        perror("fork()");
        exit(1);
    }
    if (pid == 0) // 子进程写
    {
        strcpy(ptr, "hello world !");
        munmap(ptr, MEMSIZE);
        exit(0);
    }
    else // 父进程读
    {
        wait(NULL);
        puts(ptr);
        munmap(ptr, MEMSIZE);
        exit(0);
    }
    exit(0);
}

运行结果:

liugenyi@liugenyi-virtual-machine:~/linuxsty/advio/mmap$ ./shm 
hello world !

5、文件锁

实现文件锁可以有这三个函数:fcntl()、lockf()、flock()

用法都差不多,我这里就主要以lockf为例来看看怎么用:

#include <unistd.h>

int lockf(int fd, int cmd, off_t len);
//fd:文件描述符
//cmd:命令
//len:加锁等操作的长度,如果写0表示对整个文件操作,就算这个文件未来长度发生变化也无所谓

现在我们对之前多线程阶段写过的一个例子(add.c,20个线程向同一个文件里+1,我们当时用的多线程和pthread_mutex互斥量来实现的)用多进程和文件锁来实现:

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

#define PROCNUM 20 // 进程数量
#define BUFSIZE 1024
#define FILENAME "/tmp/out"

static void func_add(void)
{
    FILE *fp;
    int fd;
    char buf[BUFSIZ];
    // 打开文件
    fp = fopen(FILENAME, "r+"); // 读写,并且保证文件存在,因此用r+
    if (fp == NULL)
    {
        perror("fopen()");
        exit(1);
    }
    // 获取文件fd
    fd = fileno(fp);
    if (fd < 0)
    {
        perror("fileno()");
        exit(1);
    }
    // 文件加锁
    lockf(fd, F_LOCK, 0);
    //  读文件
    fgets(buf, BUFSIZ, fp);
    // 定向到文件开头
    fseek(fp, 0, SEEK_SET);
    sleep(1);
    // 写文件
    fprintf(fp, "%d\n", atoi(buf) + 1);
    // 文件是全缓冲,注意刷新
    fflush(fp);
    // 文件解锁
    lockf(fd, F_ULOCK, 0);
    // 关闭文件,注意关闭文件要放在lockf后,不然会造成文件意外解锁
    fclose(fp);
    // 线程终止
    return;
}

int main()
{
    pid_t pid;
    int i;
    for (i = 0; i < PROCNUM; ++i) // 创建20个进程
    {
        pid = fork();
        if (pid < 0) // 创建子进程失败
        {
            // 我这里笼统报错退出,其实应该去收尸成功创建的子进程
            perror("fork()");
            exit(1);
        }
        if (pid == 0) // 子进程
        {
            func_add();
            exit(0);
        }
    }
    // 父进程收尸子进程
    for (i = 0; i < PROCNUM; ++i)
    {
        wait(NULL);
    }
    exit(0);
}

运行结果:

liugenyi@liugenyi-virtual-machine:~/linuxsty/advio/lockf$ echo 1 > /tmp/out
liugenyi@liugenyi-virtual-machine:~/linuxsty/advio/lockf$ cat /tmp/out 
1
liugenyi@liugenyi-virtual-machine:~/linuxsty/advio/lockf$ ./add 
liugenyi@liugenyi-virtual-machine:~/linuxsty/advio/lockf$ cat /tmp/out 
21

  • 19
    点赞
  • 34
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值