IO进程——线程、IO模型

线程与Linux IO模型详解

一、线程Thread

1、引入

1.1 概念

相当于是一个轻量级的进程,为了提高系统的性能引入线程,在同一进程中可以创建多个线程,共享进程资源

1.2 进程和线程比较

相同点:都为操作系统提供了并发执行的能力

不同点:

调度和资源:线程是系统调度的最小单位; 进程是资源分配的最小单位。

地址空间方面:一个进程创建的多个线程共享该进程资源;进程的地址空间相互独立

通信方面:线程通信相对简单。只需要通过全局变量就可以,但是需要考虑临界资源问题;进程通信比较复杂,需要借助进程间通信机制(3-4g的内核空间)

安全性方面:线程安全性差一些,当进程结束时会导致其中所有线程退出,进程相对安全

程序什么时候该使用线程?什么时候用进程?

对资源的管理和保护要求高,不限制开销和效率时,使用多进程。

要求效率高、速度快的高并发环境时,需要频繁创建、销毁或切换时,资源的保护管理要求不是很高时,使用多线程。

1.3 线程资源(了解)

共享的资源:可执行的指令、静态数据、进程中打开的文件描述符、信号处理函数、当前工作目录、用户ID、用户组ID

私有的资源:线程ID (TID)、PC(程序计数器)和相关寄存器、堆栈(局部变量, 返回地址)、错误号 (errno)、信号掩码和优先级、执行状态和属性

2、函数接口

2.1 创建线程

#include <pthread.h>

int pthread_create(pthread_t *thread, const pthread_attr_t *attr,

void *(*start_routine) (void *), void *arg);

功能:创建线程

参数:

thread ===> 线程标识

attr ===> 线程属性, NULL:代表设置默认属性

start_routine ===> 函数名:代表线程函数(自己写的)

arg ===> 用来给前面函数传参

返回值:成功:0 失败:错误码

编译的时候需要加 -pthread 链接动态库

#include<stdio.h>
#include <pthread.h>
void *handler_thread(void *arg)
{
    printf("in handler_thread\n");
    while (1);  // 让从线程不要退出
    return NULL;
}
int main(int argc, char const *argv[])  // 主线程
{
    pthread_t tid;
    if (pthread_create(&tid, NULL, handler_thread, NULL) != 0)
    {
        perror("pthread error");
        return -1;
    }
    printf("in main\n");
    while(1);   // 不要让进程结束,否则所有线程都结束了
    return 0;
}

补充:也可以给从进程传参

#include<stdio.h>
#include <pthread.h>
void *handler_thread(void *arg)
{
    printf("in handler_thread: %d\n", *(int *)arg);
    while (1);  // 让从线程不要退出
    return NULL;
}
int main(int argc, char const *argv[])  // 主线程
{
    pthread_t tid;
    int a = 100;    // 定义新的变量传输到从线程
    if (pthread_create(&tid, NULL, handler_thread, &a) != 0)
    {
        perror("pthread error");
        return -1;
    }
    printf("in main\n");
    while(1);   // 不要让进程结束,否则所有线程都结束了
    return 0;
}

2.2 退出线程

#include <pthread.h>

void pthread_exit(void *retval);

功能:用于退出线程的执行

参数:value_ptr ===> 线程退出时返回的值

#include<stdio.h>
#include <pthread.h>
void *handler_thread(void *arg)
{
    printf("in handler_thread\n");
    pthread_exit(NULL); // 让线程退出
    while (1);  // 让从线程不要退出
    return NULL;
}
int main(int argc, char const *argv[])  // 主线程
{
    pthread_t tid;
    if (pthread_create(&tid, NULL, handler_thread, NULL) != 0)
    {
        perror("pthread error");
        return -1;
    }
    printf("in main\n");
    while(1);   // 不要让进程结束,否则所有线程都结束了
    return 0;
}

2.3 回收线程资源

2.3.1 回收态

#include <pthread.h>

int pthread_join(pthread_t thread, void **value_ptr);

功能:用于等待一个指定的线程结束,阻塞函数(回收态)

参数:

        thread ===> 创建的线程对象,线程ID

        value_ptr ===> 指针*value_ptr 一般为NULL

返回值:成功:0         失败:errno

#include<stdio.h>
#include <pthread.h>
#include<unistd.h>
void *handler_thread(void *arg)
{
    printf("in handler_thread: %d\n", *(int *)arg);
    sleep(2);
    pthread_exit(NULL); // 让线程退出
    while (1);  // 让从线程不要退出
    return NULL;
}
int main(int argc, char const *argv[])  // 主线程
{
    pthread_t tid;
    int a = 100;    // 定义新的变量传输到从线程
    if (pthread_create(&tid, NULL, handler_thread, &a) != 0)
    {
        perror("pthread error");
        return -1;
    }
    pthread_join(tid, NULL);    // 阻塞等待指定线程退出回收其资源
    printf("in main\n");
    while(1);   // 不要让进程结束,否则所有线程都结束了
    return 0;
}

2.3.2 分离态

#include <pthread.h>

int pthread_detach(pthread_t thread);

功能:让线程结束时自动回收线程资源,让线程和主线程分离,非阻塞函数(分离态)

参数:thread ===> 线程ID

非阻塞式的,例如主线程分离(detach)了线程T2,那么主线程不会阻塞在pthread_detach(),pthread_detach()会直接返回,线程T2终止后会被操作系统自动回收资源

#include<stdio.h>
#include <pthread.h>
#include<unistd.h>
void *handler_thread(void *arg)
{
    printf("in handler_thread: %d\n", *(int *)arg);
    sleep(2);
    pthread_exit(NULL); // 让线程退出
    while (1);  // 让从线程不要退出
    return NULL;
}
int main(int argc, char const *argv[])  // 主线程
{
    pthread_t tid;
    int a = 100;    // 定义新的变量传输到从线程
    if (pthread_create(&tid, NULL, handler_thread, &a) != 0)
    {
        perror("pthread error");
        return -1;
    }
    pthread_detach(tid);    // 不阻塞,让指定线程退出时主动把资源还给系统
    printf("in main\n");
    while(1);   // 不要让进程结束,否则所有线程都结束了
    return 0;
}

2.4 获取线程号

pthread_t pthread_self(void);

功能:获取线程号

返回值:成功:调用此函数线程的ID

#include<stdio.h>
#include <pthread.h>
#include<unistd.h>
void *handler_thread(void *arg)
{
    printf("in handler_thread: %ld\n", pthread_self()); // 获取进程号
    pthread_exit(NULL); // 让线程退出
    while (1);  // 让从线程不要退出
    return NULL;
}
int main(int argc, char const *argv[])  // 主线程
{
    pthread_t tid;
    if (pthread_create(&tid, NULL, handler_thread, NULL) != 0)
    {
        perror("pthread error");
        return -1;
    }
    printf("in main\n");
    while(1);   // 不要让进程结束,否则所有线程都结束了
    return 0;
}

3、练习

        通过线程实现数据的交互,主线程循环从终端输入,线程函数将数据循环输出,当输入quit结束程序。

#include<stdio.h>
#include <pthread.h>
#include<string.h>
char buf[32];
int flag = 0;   // 设置标志位判断是否输入输出完成,0代表可以输入,1代表可以输出
void *handler_thread(void *arg)
{
    while (1)   // 从线程不断输出
    {
        if (flag == 1)  // 1才可以输出
        {        
            if (!strcmp(buf, "quit"))
                break;
            printf("%s\n", buf);
            flag = 0;   // 输入完置0代表可以输入
        }
    }
    pthread_exit(NULL); // 让从线程退出
    return NULL;
}
int main(int argc, char const *argv[])  // 主线程
{
    pthread_t tid;
    if (pthread_create(&tid, NULL, handler_thread, NULL) != 0)
    {
        perror("pthread error");
        return -1;
    }
    while (1)   // 主线程不断输入
    {
        if (flag == 0)  // 0才可以输入
        {
            scanf("%s", buf);
            flag = 1;   // 输入完置1代表可以输出
            if (!strcmp(buf, "quit"))
                break;   
        }     
    }
    pthread_detach(tid);    // 不阻塞,让指定线程退出时主动把资源还给系统
    return 0;
}

4、同步

4.1概念

        同步(synchronization)指的是多个任务(线程)按照约定的顺序相互配合完成一件事情 (异步:异步则反之,并非要按照顺序完成事件)

4.2 同步机制

通过信号量实现线程间同步

信号量:通过信号量实现同步操作;由信号量来决定线程是继续运行还是阻塞等待

信号量代表某一类资源,其值表示系统中该资源的数量

信号量的值>0表示有资源可以用, 可以申请到资源

信号量的值<=0表示没有资源可以用, 无法申请到资源, 阻塞

信号量还是一个受保护的变量,只能通过三种操作来访问:初始化、P操作(申请资源)、V操作(释放资源)

sem_init: 信号量初始化

sem_wait: 申请资源P操作, 如果没有资源可以用阻塞-1

sem_post: 释放资源V操作, 非阻塞 +1

4.3 函数接口(信号量)

4.3.1 初始化信号量

#include <semaphore.h>

int sem_init(sem_t *sem, int pshared, unsigned int value);

功能:初始化信号量

参数:sem:初始化的信号量对象

pshared:信号量共享的范围(0: 线程间使用 非01进程间使用)

value:信号量初值

返回值:成功 0         失败 -1

4.3.2 申请资源

#include <semaphore.h>

int sem_wait(sem_t *sem);

功能:申请资源 P操作

参数:sem:信号量对象

返回值:成功 0 失败 -1

注:此函数执行过程,当信号量的值大于0时,表示有资源可以用,则继续执行,同时对信号量减1;当信号量的值等于0时,表示没有资源可以使用,函数阻塞

4.3.3 释放资源

#include <semaphore.h>

int sem_post(sem_t *sem);

功能:释放资源 V操作

参数:sem:信号量对象

返回值:成功 0 失败 -1

注:释放一次信号量的值加1,函数不阻塞

4.4 练习

        通过线程实现数据的交互,主线程循环从终端输入,线程函数将数据循环输出,当输入quit结束程序。

双信号量:

#include<stdio.h>
#include <semaphore.h>
#include <pthread.h>
#include<string.h>
char buf[32];
sem_t sem1, sem2;
void *handler_thread(void *arg)
{
    while (1)   // 从线程不断输出
    {
        sem_wait(&sem1); // 申请资源
        if (!strcmp(buf, "quit"))
            break;
        printf("%s\n", buf);
        sem_post(&sem2); // 释放资源
    }
    pthread_exit(NULL); // 让从线程退出
    return NULL;
}
int main(int argc, char const *argv[])  // 主线程
{
    pthread_t tid;  // 创线程
    // 初始化信号量
    if (sem_init(&sem1, 0, 0) != 0)
    {
        perror("sem init error");
        return -1;
    }
    if (sem_init(&sem2, 0, 1) != 0)
    {
        perror("sem init error");
        return -1;
    }   
    if (pthread_create(&tid, NULL, handler_thread, NULL) != 0)
    {
        perror("pthread error");
        return -1;
    }
    while (1)   // 主进程不断输入
    {
        sem_wait(&sem2); // 申请资源
        scanf("%s", buf);
        if (!strcmp(buf, "quit"))
            break;
        sem_post(&sem1); // 释放资源
    }
    pthread_detach(tid);    // 不阻塞,让指定线程退出时主动把资源还给系统
    return 0;
}

单信号量:

#include<stdio.h>
#include <semaphore.h>
#include <pthread.h>
#include<string.h>
char buf[32];
sem_t sem;
void *handler_thread(void *arg)
{
    while (1)   // 从线程不断输出
    {     
        sem_wait(&sem); // 申请资源
        if (!strcmp(buf, "quit"))
            break;
        printf("%s\n", buf);
    }
    pthread_exit(NULL); // 让从线程退出
    return NULL;
}
int main(int argc, char const *argv[])  // 主线程
{
    pthread_t tid;  // 创线程
    // 初始化信号量
    if (sem_init(&sem, 0, 0) != 0)
    {
        perror("sem init error");
        return -1;
    }   
    if (pthread_create(&tid, NULL, handler_thread, NULL) != 0)
    {
        perror("pthread error");
        return -1;
    }
    while (1)   // 主进程不断输入
    {
        scanf("%s", buf);
        if (!strcmp(buf, "quit"))
            break;
        sem_post(&sem); // 释放资源
    }
    pthread_detach(tid);    // 不阻塞,让指定线程退出时主动把资源还给系统
    return 0;
}

5、互斥

5.1 概念

多个线程在访问临界资源时,同一时间只能一个线程访问。

临界资源:一次仅允许一个线程所使用的资源

临界区:一个访问共享资源的程序片段

互斥锁(mutex): 通过互斥锁可以实现互斥机制,主要用来保护临界资源,每个临界资源都由一个互斥锁来保护,线程必须先获得互斥锁才能访问临界资源,访问完资源后释放该锁。如果无法获得锁,线程会阻塞直到获得锁为止。

5.2 函数接口

5.2.1 初始化互斥锁

#include <pthread.h>

int pthread_mutex_init(pthread_mutex_t *mutex, pthread_mutexattr_t *attr)

功能:初始化互斥锁

参数:mutex:互斥锁

attr: 互斥锁属性 // NULL表示缺省属性

返回值:成功 0 失败 -1

5.2.2 申请互斥锁

#include <pthread.h>

int pthread_mutex_lock(pthread_mutex_t *mutex)

功能:申请互斥锁

参数:mutex:互斥锁

返回值:成功 0

失败 -1

注:和pthread_mutex_trylock区别:pthread_mutex_lock是阻塞的;pthread_mutex_trylock不阻塞,如果申请不到锁会立刻返回

5.2.3 释放互斥锁

#include <pthread.h>

int pthread_mutex_unlock(pthread_mutex_t *mutex)

功能:释放互斥锁

参数:mutex:互斥锁

返回值:成功 0

失败 -1

5.2.4 销毁互斥锁

#include <pthread.h>

int pthread_mutex_destroy(pthread_mutex_t *mutex)

功能:销毁互斥锁

参数:mutex:互斥锁

5.3 练习

通过互斥锁实现打印倒置数组功能

#include <pthread.h>
#include<stdio.h>
#include <unistd.h>     /*sleep头文件*/
#define N 10
int a[N] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};  // 定义数组
pthread_mutex_t lock;   // 定义一把锁
/*==========从线程:倒置函数==========*/
void *swap(void *arg)
{
    int temp = 0;   /*定义中间变量用于交换*/
    while (1)
    {
        pthread_mutex_lock(&lock);  /*上锁*/
        for (int i = 0; i < N / 2; i++)
        {
            temp = a[i];
            a[i] = a[N - 1 - i];
            a[N - 1 - i] = temp;
        }
        pthread_mutex_unlock(&lock);    /*解锁*/
    }
    return NULL;
}
/*==========从线程:打印函数==========*/
void *print(void *arg)
{
    while (1)
    {
        pthread_mutex_lock(&lock);  /*上锁*/
        for (int i = 0; i < N; i++)
            printf("%d ", a[i]);
        putchar(10);
        pthread_mutex_unlock(&lock);  /*解锁*/ 
        sleep(1);   /*锁里面减少耗时大的操作*/       
    }
    return NULL;
}
/*==========主线程==========*/
int main(int argc, char const *argv[])
{
    pthread_t tid1, tid2;
    // 1.初始化互斥锁
    if(pthread_mutex_init(&lock, NULL) != 0)
    {
        perror("pthread_mutex_init error");
        return -1;
    }
    // 2.创建线程
    /*==1>创建从线程 1 用于倒置数组==*/
    if (pthread_create(&tid1, NULL, swap, NULL) != 0)
    {
        perror("pthread_create swap error");
        return -1;
    }
    /*==2>创建从线程 2 用于打印数组==*/
    if (pthread_create(&tid2, NULL, print, NULL) != 0)
    {
        perror("pthread_create print error");
        return -1;
    }
    // 3.防止主线程结束,进行阻塞回收从线程资源
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);   
    return 0;
}

5.4 死锁

是指两个或两个以上的进程或线程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。

●死锁产生的四个必要条件

1、互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用

2、不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。

3、请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。

4、循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了一个等待环路。

注意:当上述四个条件都成立的时候,便形成死锁。当然,死锁的情况下如果打破上述任何一个条件,便可让死锁消失。

5.5 条件变量

5.5.1 概念

条件变量用于在线程之间传递信号,以便某些线程可以等待某些条件发生。当某些条件发生时,条件变量会发出信号,使等待该条件的线程可以恢复执行。

假设想先运行线程A,再运行线程B:

因为想要先运行A线程,所以需要先将B进程阻塞,故进程开始时先让A线程睡一会,先去调度B线程,

5.5.2 函数接口

一般和互斥锁搭配使用,实现同步机制:

pthread_cond_init(&cond,NULL); //初始化条件变量

使用前需要上锁:

pthread_mutex_lock(&lock); //上锁

一些逻辑:

ptread_cond_wait(&cond, &lock); //阻塞等待条件产生,没有条件产生时阻塞,同时解锁,当条件产生时结束阻塞,再次上锁。

执行任务:

pthread_mutex_unlock(&lock); //解锁

pthread_cond_signal(&cond); //产生条件,不阻塞

pthread_cond_destroy(&cond); //销毁条件变量

注意: 必须保证让pthread_cond_wait先执行,然后再pthread_cond_signal产生条件。

#include <pthread.h>
#include<stdio.h>
#include <unistd.h>     /*sleep头文件*/
#define N 10
int a[N] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};  // 定义数组
pthread_mutex_t lock;   // 定义一把锁
pthread_cond_t cond;    // 条件变量
/*==========从线程:倒置函数==========*/
void *swap(void *arg)
{
    int temp = 0;   /*定义中间变量用于交换*/
    while (1)
    {
        pthread_mutex_lock(&lock);  /*上锁*/
        // 等待条件产生
        pthread_cond_wait(&cond, &lock);
        for (int i = 0; i < N / 2; i++)     /*倒置数组*/
        {
            temp = a[i];
            a[i] = a[N - 1 - i];
            a[N - 1 - i] = temp;
        }
        pthread_mutex_unlock(&lock);    /*解锁*/
    }
    return NULL;
}
/*==========从线程:打印函数==========*/
void *print(void *arg)
{
    while (1)
    {
        sleep(1);   /*锁里面减少耗时大的操作*/ 
        pthread_mutex_lock(&lock);  /*上锁*/
        for (int i = 0; i < N; i++) /*循环打印数组*/
            printf("%d ", a[i]);
        putchar(10);
        pthread_cond_signal(&cond); /*产生条件,不阻塞*/
        pthread_mutex_unlock(&lock);  /*解锁*/       
    }
    return NULL;
}
/*==========主线程==========*/
int main(int argc, char const *argv[])
{
    pthread_t tid1, tid2;
    // 1.初始化互斥锁
    if(pthread_mutex_init(&lock, NULL) != 0)
    {
        perror("pthread_mutex_init error");
        return -1;
    }
    // 2.初始化条件变量
    if (pthread_cond_init(&cond, NULL) != 0)
    {
        perror("cond init err");
        return - 1;
    }
    // 3.创建线程
    /*==1>创建从线程 1 用于倒置数组==*/
    if (pthread_create(&tid1, NULL, swap, NULL) != 0)
    {
        perror("pthread_create swap error");
        return -1;
    }
    /*==2>创建从线程 2 用于打印数组==*/
    if (pthread_create(&tid2, NULL, print, NULL) != 0)
    {
        perror("pthread_create print error");
        return -1;
    }
    // 4.防止主线程结束,进行阻塞回收从线程资源
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);
    pthread_mutex_destroy(&lock);
    pthread_cond_destroy(&cond);
    return 0;
}

6、同步&互斥总结

互斥:两个线程之间不可以同时运行,他们会相互排斥,必须等待一个线程运行完毕,另一个才能运行。

同步:两个线程之间也不可以同时运行,但他是必须要按照某种次序来运行相应的线程(也可以说是一种互斥)!

所以说:同步是一种更为复杂的互斥,而互斥是一种特殊的同步。

二、Linux IO 模型

  • 场景假设1

假设妈妈有一个孩子,孩子在房间里睡觉,妈妈需要及时获知孩子是否醒了,如何做?

  1. 妈妈在房间呆着,和孩子一起睡:妈妈不累,但是不能干其他事情。
  2. 时不时看一下孩子,其他事件可以干一点其他事情:累,但是可以干其他事情。
  3. 妈妈在客厅玩,听孩子是否哭了:二者互不耽误

1、阻塞式IO:最常见、效率低、不浪费CPU

阻塞I/O 模式是最普遍使用的I/O 模式,大部分程序使用的都是阻塞模式的I/O 。

学习的读写函数在调用过程中会发生阻塞,相关函数如下:

•读操作中的read

读阻塞--> 需要读缓冲区中有数据可读,读阻塞解除

•写操作中的write

写阻塞--> 阻塞情况比较少,主要发生在写入的缓冲区的大小小于要写入的数据量的情况下,写操作不进行任何拷贝工作,将发生阻塞,一旦缓冲区有足够的空间,内核将唤醒进程,将数据从用户缓冲区拷贝到相应的发送数据缓冲区。

2、非阻塞式IO:轮询、耗费CPU、可以同时处理多路IO

•当我们设置为非阻塞模式,我们相当于告诉了系统内核:“当我请求的I/O 操作不能够马上完成,你想让我的进程进行休眠等待的时候,不要这么做,请马上返回一个错误给我。”

•当一个应用程序使用了非阻塞模式的套接字,它需要使用一个循环来不停地测试是否一个文件描述符有数据可读(称做polling)。

•应用程序不停的polling 内核来检查是否I/O操作已经就绪。这将是一个极浪费CPU 资源的操作。

•这种模式使用中不普遍。

2.1 通过函数自带参数设置

IPC_NOWAIT:非阻塞,不管有没有消息都立刻返回,所以有可能会读不到消息需要轮询

2.2 通过设置文件描述符的属性设置非阻塞

#include <unistd.h>

#include <fcntl.h>

int fcntl(int fd, int cmd, ... /* arg */ ); 

功能:设置文件描述符属性

参数:

fd:文件描述符

cmd:设置方式 - 功能选择

F_GETFL 获取文件描述符的状态信息 第三个参数化忽略

F_SETFL 设置文件描述符的状态信息 通过第三个参数设置

O_NONBLOCK 非阻塞

O_ASYNC 异步

O_SYNC 同步

arg:设置的值 in

返回值:

特殊选择返回特殊值 - F_GETFL 返回的状态值(int)

其他:成功0 失败-1,更新errno

使用:0为例子 0原本:阻塞、读权限-->修改或添加为非阻塞 int flags=fcntl(0,F_GETFL); //1.获取文件描述符的原有的属性 flags=flags | O_NONBLOCK; //2.修改添加模式为非阻塞 fcntl(0,F_SETFL,flags); //3.设置修改后的模式

#include<stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include<string.h>
int main(int argc, char const *argv[])
{
    // 1.获取文件描述符的属性
    int flags = fcntl(0, F_GETFL); 
    // 2.修改添加描述符属性为阻塞
    flags |= O_NONBLOCK;
    // 3.设置文件描述符的属性
    fcntl(0, F_SETFL, flags);
    // 4.实验非阻塞模式
    char buf[32] = "";
    while (1)
    {
        sleep(2);       
        fgets(buf, sizeof(buf), stdin);
        printf("buf: %s\n", buf);
        memset(buf, 0, sizeof(buf));
        printf("===========================\n");
    }
    return 0;
}

会发现不等待用户输入直接打印,但是也不影响输入

注意:恢复阻塞模式需要关闭终端,换个终端才生效

或者设置回去:

flag &= ~O_NONBLOCK;

fcntl(0, F_SETFL, flag);

#include<stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include<string.h>
int main(int argc, char const *argv[])
{
    // 1.获取文件描述符的属性
    int flags = fcntl(0, F_GETFL); 
    // 2.修改添加描述符属性为阻塞
    flags |= O_NONBLOCK;
    // 3.设置文件描述符的属性
    fcntl(0, F_SETFL, flags);
    // 4.恢复阻塞模式
    flags &= ~O_NONBLOCK;
    fcntl(0, F_SETFL, flags);
    // 5.实验非阻塞模式
    char buf[32] = "";
    while (1)
    {
        sleep(2);       
        fgets(buf, sizeof(buf), stdin);
        printf("buf: %s\n", buf);
        memset(buf, 0, sizeof(buf));
        printf("===========================\n");
    }
    return 0;
}

3、信号驱动IO:异步通知方式,底层驱动的支持

查看鼠标是哪个文件:

信号驱动I/O是一种异步I/O模型,通过操作系统向应用程序发送信号来通知数据可读或可写,从而避免轮询或阻塞等待。

异步通知:异步通知是一种非阻塞的通知机制,发送方发送通知后不需要等待接收方的响应或确认。通知发送后,发送方可以继续执行其他操作,而无需等待接收方处理通知。

1.通过信号方式,当内核检测到设备数据后,会主动给应用发送信号SIGIO。

2.应用程序收到信号后做异步处理即可。

3.应用程序需要把自己的进程号告诉内核,并打开异步通知机制。

//1.将设置文件描述符和进程号递交给内核驱动
//一旦fd有事件响应,则内核驱动会给进程发送一个SIGIO信号
fcntl(fd,F_SETOWN,getpid());

//2.设置异步通知
int flags=fcntl(fd,F_GETFL);//获取原来描述符属性
flags|=O_ASYNC;//将属性设置为异步
fcntl(fd,F_SETFL,flags); //将修改的属性设置进去

//3.signal捕捉SIGIO信号--SIGIO信号是内核通知进程的信号
signal(SIGIO,handler);



#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <signal.h>
int fd;
void handler(int sig)
{
    char buf[32] = "";
    read(fd, buf, sizeof(buf));
    printf("%s\n", buf);
}
int main(int argc, char const *argv[])
{
    fd = open("/dev/input/mouse0", O_RDONLY);
    if (fd < 0)
    {
        perror("open err");
        return -1;
    }
    printf("fd:%d\n", fd);
    //1.将设置文件描述符和进程号递交给内核驱动
    //一旦fd有事件响应,则内核驱动会给进程发送一个SIGIO信号
    fcntl(fd, F_SETOWN, getpid());
    //2.设置异步通知
    int flags = fcntl(fd, F_GETFL); //获取原来描述符属性
    flags |= O_ASYNC;               //将属性设置为异步
    fcntl(fd, F_SETFL, flags);      //将修改的属性设置进去
    //3.signal捕捉SIGIO信号--SIGIO信号是内核通知进程的信号
    signal(SIGIO, handler);
    while (1)
    {
        printf("玩一玩\n");
        sleep(1);
    }
    return 0;
}

阻塞IO(Blocking IO)

非阻塞IO

(Non-blocking IO)

信号驱动IO(Signal-driven IO)

同步性

同步

同步

异步

描述

调用IO操作的线程会被阻塞,直到操作完成

调用IO操作时,如果不能立即完成操作,会立即返回,线程可以继续执行其他操作

当IO操作可以进行时,内核会发送信号通知进程

特点

最常见、效率低、不耗费cpu,

轮询、耗费CPU,可以处理多路IO,效率高

异步通知方式,需要底层驱动的支持

适应场景

小规模IO操作,对性能要求不高

高并发网络服务器,减少线程阻塞时间

实时性要求高的应用,避免轮询开销

  • 场景假设2

假设妈妈有三个孩子,分别不同的房间里睡觉,需要及时获知每个孩子是否醒了,如何做?

阻塞IO?在一个房间,不行

非阻塞IO?不停的每个房间查看,可以

信号驱动IO?不行,因为只有一个信号,不知道哪个孩子醒了

方案:

1不停的每个房间查看:超级无敌累,但是也可以干点别的事

2妈妈在客厅睡觉,雇保姆孩子醒了让保姆抱着找妈妈:即可以休息,也可以及时获取状态。

4、IO多路复用:select/poll/epoll

(1)应用程序中同时处理多路输入输出流,若采用阻塞模式,得不到预期的目的;

(2)若采用非阻塞模式,对多个输入进行轮询,但又太浪费CPU时间;

(3)若设置多个进程/线程,分别处理一条数据通路,将新产生进程/线程间的同步与通信问题,使程序变得更加复杂;

(4)比较好的方法是使用I/O多路复用技术。其基本思想是:

○ 先构造一张有关描述符的表(最大1024),然后调用一个函数。

○ 当这些文件描述符中的一个或多个已准备好进行I/O时函数才返回。

○ 函数返回时告诉进程那个描述符已就绪,可以进行I/O操作。

4.1 select

4.1.1 特点

特点:

1.一个进程最多只能监听1024个文件描述符

2.select被唤醒之后要重新轮询,效率相对低

3.select每次都会清空未发生响应的文件描述符,每次拷贝都需要从用户空间到内核空间,效率低,开销大

4.1.2 步骤

第一步:构造一张关于文件描述符的表

第二步:清空表FD_ZERO

第三步:将关心的文件描述符添加到表中FD_SET

第四步:调用select函数

第五步:判断哪个或哪些文件描述符产生了事件FD_ISSET

第六步:做对应的逻辑处理

4.1.3 函数接口

int select(int nfds, fd_set *readfds, fd_set *writefds,

fd_set *exceptfds, struct timeval *timeout);

功能:

实现IO的多路复用

参数:

nfds:关注的最大的文件描述符+1

readfds:关注的读表

writefds:关注的写表

exceptfds:关注的异常表

timeout:超时的设置

NULL:一直阻塞,直到有文件描述符就绪或出错

时间值为0:仅仅检测文件描述符集的状态,然后立即返回

时间值不为0:在指定时间内,如果没有事件发生,则超时返回0,并清空设置的时间值

struct timeval

{

long tv_sec; /* 秒 */

long tv_usec; /* 微秒 = 10^-6秒 */

};

返回值:

成功时返回准备好的文件描述符的个数

0:超时检测时间到并且没有文件描述符准备好

-1 :失败

注意:select返回后,关注列表中只存在准备好的文件描述符

操作表:

void FD_CLR(int fd, fd_set *set); //清除集合中的fd位

void FD_SET(int fd, fd_set *set); //将fd放入关注列表中

int FD_ISSET(int fd, fd_set *set); //判断fd是否产生操作 是:1 不是:0

void FD_ZERO(fd_set *set); //清空关注列表

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/select.h>
#include <sys/time.h>
#include <string.h>
int main(int argc, char const *argv[])
{
    // 1.打开鼠标文件描述符
    int fd_mouse = open("/dev/input/mouse0", O_RDONLY);
    if (fd_mouse < 0)
    {
        perror("fd_mouse error");
        return -1;
    }
    printf("fd_mouse: %d\n", fd_mouse);
    // 2.创建文件描述符的表
    fd_set readfds;
    while (1)   /*select返回后表被内核修改了,所以每次循环都要清空并重新添加描述符到表中*/
    {
        // 3.清空表
        FD_ZERO(&readfds);
        // 4.将关心的文件描述符添加到表中
        /*= 1>鼠标 =*/
        FD_SET(fd_mouse, &readfds);
        /*= 2>键盘 =*/
        FD_SET(0, &readfds);
        // 5.监听是否有描述符发生操作
        if (select(fd_mouse + 1, &readfds, NULL, NULL, NULL) < 0)
        {
            perror("select error");
            return -1;
        }
        printf("something happend!\n");
        // 6.判断是哪个文件描述符发生了操作
        /*= 1>鼠标 =*/
        char buf[32] = "";
        if (FD_ISSET(fd_mouse, &readfds))
        {
            ssize_t n = read(fd_mouse, buf, sizeof(buf) - 1);   /*预留一个空间给'\0'*/
            buf[n] = '\0';
            printf("mouse: %s\n", buf);  /*手动在末尾添加'\0',因为read不会补'\0'*/
        }
        /*= 2>键盘 =*/
        if (FD_ISSET(0, &readfds))
        {
            scanf("%s", buf);
            printf("keybord: %s\n", buf);
        }
    }   
    close(fd_mouse);
    return 0;
}

4.1.4 超时检测

概念

什么是网络超时检测呢,比如某些设备的规定,发送请求数据后,如果多长时间后没有收到来自设备的回复,那么需要做出一些特殊的处理

比如: 链接wifi的时候,等了好长时间也没有连接上,此时系统会发送一个消息: 网络连接失败;

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/select.h>
#include <sys/time.h>
#include <string.h>
int main(int argc, char const *argv[])
{
    // 1.打开鼠标文件描述符
    int fd_mouse = open("/dev/input/mouse0", O_RDONLY);
    if (fd_mouse < 0)
    {
        perror("fd_mouse error");
        return -1;
    }
    printf("fd_mouse: %d\n", fd_mouse);
    // 2.创建文件描述符的表
    fd_set readfds;
    while (1)   /*select返回后表被内核修改了,所以每次循环都要清空并重新添加描述符到表中*/
    {
        // 3.清空表
        FD_ZERO(&readfds);
        // 4.将关心的文件描述符添加到表中
        /*= 1>鼠标 =*/
        FD_SET(fd_mouse, &readfds);
        /*= 2>键盘 =*/
        FD_SET(0, &readfds);
        // 超时检测
        struct timeval tm = {2, 0}; /*定时2秒*/
        // 5.监听是否有描述符发生操作
        if (select(fd_mouse + 1, &readfds, NULL, NULL, &tm) < 0)
        {
            perror("select error");
            return -1;
        }
        else if (select(fd_mouse + 1, &readfds, NULL, NULL, &tm) == 0)    // 到时间IO还没有准备就绪
        {
            perror("Time's up");
            continue;
        }   
        printf("something happend!\n");
        // 6.判断是哪个文件描述符发生了操作
        /*= 1>鼠标 =*/
        char buf[32] = "";
        if (FD_ISSET(fd_mouse, &readfds))
        {
            ssize_t n = read(fd_mouse, buf, sizeof(buf) - 1);   /*预留一个空间给'\0'*/
            buf[n] = '\0';
            printf("mouse: %s\n", buf);  /*手动在末尾添加'\0',因为read不会补'\0'*/
        }
        /*= 2>键盘 =*/
        if (FD_ISSET(0, &readfds))
        {
            scanf("%s", buf);
            printf("keybord: %s\n", buf);
        }
    }   
    close(fd_mouse);
    return 0;
}
必要性
  1. 避免进程在没有数据时无限制的阻塞;
  2. 规定时间未完成语句应有的功能,则会执行相关功能

4.2 poll

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

功能:同select相同实现IO的多路复用

参数:

fds:指向一个结构体数组的指针,用于指定测试某个给定的文件描述符的条件

nfds:指定的第一个参数数组的元素个数。

timeout:超时设置

-1:永远等待

0:立即返回

>0:等待指定的毫秒数

struct pollfd

{

int fd; // 文件描述符

short events; // 等待的事件

short revents; // 实际发生的事件

};

返回值:

成功时返回结构体中 revents 域不为 0 的文件描述符个数

0: 超时前没有任何事件发生时,返回 0

-1:失败并设置 errno

特点:

(1)优化文件描述符的限制,文件描述符的限制取决于系统

(2)poll被唤醒之后要重新轮询一遍,效率相对低

(3)poll不需要重新构造表,采用结构体数组,每次都需要从用户空间拷贝到内核空间

4.2.1 实现过程

(1)创建一张表,也就是一个结构体数组struct pollfd fds[1000];

(2)添加关心的描述符到表中

(3)循环poll监听更新表

(4)逻辑判断

#include <stdio.h>
#include <sys/poll.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>

int main(int argc, char const *argv[])
{
    int fd = open("/dev/input/mouse0", O_RDONLY);
    if (fd < 0)
    {
        perror("open err");
        return -1;
    }
    //1.创建表也就是结构体数组
    struct pollfd fds[2];

    //2. 将关心的文件描述符添加到表中,并赋予事件
    fds[0].fd = 0;          //键盘
    fds[0].events = POLLIN; //想要发生的事件是读事件
    
    fds[1].fd = fd;         //鼠标
    fds[1].events = POLLIN;

    //3.保存数组内最后一个有效元素下标
    int last = 1;

    //4.循环调用poll监听
    while (1)
    {
        int ret = poll(fds, last + 1, 2000);
        if (ret < 0)
        {
            perror("poll err");
            return -1;
        }
        else if (ret == 0)
        {
            printf("time out\n");
            continue;
        }

        //5.判断结构体内文件描述符实际发生的事件
        char buf[32] = "";
        //键盘
        if (fds[0].revents == POLLIN)
        {
            //6.根据不同的文件描述符发生的不同事件做对应的逻辑处理
            fgets(buf, sizeof(buf), stdin);
            printf("keyboard: %s\n", buf);
        }
        //鼠标
        if (fds[1].revents == POLLIN)
        {
            ssize_t n = read(fd, buf, sizeof(buf) - 1);
            buf[n] = '\0';
            printf("mouse: %s\n", buf);
        }
    }
    close(fd);
    return 0;
}

4.3 epoll

特点:

  1. 监听的最大的文件描述符没有个数限制
  2. 异步IO,epoll当有事件产生被唤醒之后,文件描述符主动调用callback函数(回调函数)直接拿到唤醒的文件描述符,不需要轮询,效率高
  3. epoll不需要重新构造文件描述符表,只需要从用户空间拷贝到内核空间一次。

4.4 总结

select

poll

epoll

监听个数

一个进程最多监听1024个文件描述符

由程序员自己决定

百万级

方式

每次都会被唤醒,都需要重新轮询

每次都会被唤醒,都需要重新轮询

红黑树内callback自动回调,不需要轮询

效率

文件描述符数目越多,轮询越多,效率越低

文件描述符数目越多,轮询越多,效率越低

不轮询,效率高

原理

每次使用select后,都会清空表

每次调用select,都需要拷贝用户空间的表到内核空间

内核空间负责轮询监视表内的文件描述符,将发生事件的文件描述符拷贝到用户空间,再次调用select,如此循环

不会清空结构体数组

每次调用poll,都需要拷贝用户空间的结构体到内核空间

内核空间负责轮询监视结构体数组内的文件描述符,将发生事件的文件描述符拷贝到用户空间,再次调用poll,如此循环

不会清空表

epoll中每个fd只会从用户空间到内核空间只拷贝一次(上树时)

通过epoll_ctl将文件描述符交给内核监管,一旦fd就绪,内核就会采用callback的回调机制来激活该fd,epoll_wait便可以收到通知(内核空间到用户空间的拷贝

特点

一个进程最多能监听1024个文件描述符

select每次被唤醒,都要重新轮询表,效率低

select每次都清空未发生相应的文件描述符,每次都要拷贝用户空间的表到内核空间

优化文件描述符的个数限制

poll每次被唤醒,都要重新轮询,效率比较低(耗费cpu)

poll不需要构造文件描述符表(也不需要清空表),采用结构体数组,每次也需要从用户空间拷贝到内核空间

监听的文件描述符没有个数限制(取决于自己的系统)

异步IO,epoll当有事件产生被唤醒,文件描述符会主动调用callback函数拿到唤醒的文件描述符,不需要轮询,效率高

epoll不需要构造文件描述符的表,只需要从用户空间拷贝到内核空间一次。

结构

数组

数组

红黑树+就绪链表

开发复杂度

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值