IO进程——线程、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不需要构造文件描述符的表,只需要从用户空间拷贝到内核空间一次。

结构

数组

数组

红黑树+就绪链表

开发复杂度

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

人工智能概念:NLP任务的评估指标(BLEU、ROUGE、PPL、BERTScore、RAGAS)

文章目录一、评估指标基础1. 准确率&#xff08;Accuracy&#xff09;2. 精确率&#xff08;Precision&#xff09;3. 召回率&#xff08;Recall&#xff09;4. F1-Score5. 示例二、文本生成专用指标1. BLEU&#xff1a;机器翻译与标准化文案的“质量标尺”1.1 计算流程&#x…

团队对 DevOps 理解不统一会带来哪些问题

团队对DevOps理念与实践的理解不统一、片面甚至扭曲&#xff0c;是导致众多企业DevOps转型失败的根本原因&#xff0c;它将直接引发一系列深层次的、相互关联的严重问题。核心体现在&#xff1a;转型极易沦为“为了工具而工具”的盲目自动化&#xff0c;导致最核心的文化变革被…

企业级实战:构建基于Qt、C++与YOLOv8的模块化工业视觉检测系统(基于QWidget)

目录一、概述二、项目目标与技术架构2.1 核心目标2.2 技术选型2.3 软件架构三、AI推理DLL的开发 (Visual Studio 2019)3.1 定义DLL接口 (DetectorAPI.h)3.2 实现核心功能 (DetectorAPI.cpp)四、Qt Widget GUI应用程序的开发4.1 项目配置 (.pro 文件)4.2 UI设计 (mainwindow.ui)…

SVN自动化部署工具 脚本

SVN自动化部署工具 功能概述 这是一个自动化部署SVN仓库的bash脚本&#xff0c;主要功能包括&#xff1a; 自动安装SVN服务&#xff08;如未安装&#xff09; 创建SVN项目仓库 配置多用户权限 设置自动同步到网站目录 提供初始检出功能 下载地址 https://url07.ctfile…

Facebook主页变现功能被封?跨境玩家该如何申诉和预防

不少跨境玩家在运营Facebook公共主页时&#xff0c;最期待的就是通过变现工具获得稳定收入。但现实中&#xff0c;经常会遇到一个扎心的问题&#xff1a;主页好不容易做起来&#xff0c;却突然收到提示——“你的变现功能已被停用”。这意味着收入中断&#xff0c;甚至可能导致…

安装es、kibana、logstash

下载 elk 下载地址 elasticsearch地址: https://www.elastic.co/cn/downloads/elasticsearch kibana地址: https://www.elastic.co/cn/downloads/kibana logstash地址: https://www.elastic.co/cn/downloads/logstash 解压elk 创建es全家桶文件夹 cd /usr/local mkdir elk …

Django admin 后台开发案例【字段/图片】

这是一个简单的django admin 管理后台,这个应用案例主要是给运营人员进行填写数据 主要功能包括: 上传图片功能【选择上传时可以预览】【替换已有数据中的图片时可以预览新旧图片】 每条数据都将会记录操作历史。记录操作人是谁?修改内容是什么?并且定位责任到某一员。 …

【C++】const和static的用法

目录&#x1f680;前言&#x1f4bb;const&#xff1a;“只读”的守护者&#x1f4af;修饰普通变量&#x1f4af;修饰指针&#x1f4af;修饰函数&#x1f4af;修饰类成员&#x1f4af;修饰对象&#x1f31f;static&#xff1a;“静态存储”与“作用域控制”&#x1f4af;修饰全…

F019 vue+flask海外购商品推荐可视化分析系统一带一路【三种推荐算法】

文章结尾部分有CSDN官方提供的学长 联系方式名片 B站up&#xff1a; 麦麦大数据 关注B站&#xff0c;有好处&#xff01; 编号: F019 关键词&#xff1a;海外购 推荐系统 一带一路 python 视频 VueFlask 海外购电商大数据推荐系统源码 &#xff08;三种推荐算法 全新界面布局…

【大数据专栏】流式处理框架-Apache Fink

Apache Fink 1 前言 1.1 功能 1.2 用户 国际 国内 1.3 特点 ◆ 结合Java、Scala两种语言 ◆ 从基础到实战 ◆ 系统学习Flink的核心知识 ◆ 快速完成从入门到上手企业开发的能力提升 1.4 安排 ◆ 初识Flink ◆ 编程模型及核心概念 ◆ DataSet API编程 ◆ Data…

向内核社区提交补丁

一、背景 内核的版本一直以来一直在持续迭代&#xff0c;离不开众多开发者的贡献。有时候我们会根据项目要求基于现有的内核版本开发一些新的功能或者修复掉一些特定场下的问题&#xff0c;我们是可以将其提交给社区的。 一般提交社区有两个基本原则&#xff0c;一是提交的补…

TENGJUN-USB TYPE-C 24PIN测插双贴连接器(H14.3,4脚插板带柱):USB4.0高速传输时代的精密连接方案解析

在高速数据传输与多设备互联需求日益增长的当下&#xff0c;USB TYPE-C接口凭借其可逆插拔、高兼容性的优势成为主流&#xff0c;而TENGJUN推出的USB TYPE-C 24PIN测插双贴连接器&#xff08;规格&#xff1a;H14.3&#xff0c;4脚插板带柱&#xff09; &#xff0c;以对USB4.0…

企业级 Docker 应用:部署、仓库与安全加固

1 Docker简介及部署方法 1.1 Docker简介 Docker之父Solomon Hykes&#xff1a;Docker就好比传统的货运集装箱 Note 2008 年LXC(LinuX Contiainer)发布&#xff0c;但是没有行业标准&#xff0c;兼容性非常差 docker2013年首次发布&#xff0c;由Docker, Inc开发1.1.1 什么是do…

rust语言 (1.88) 学习笔记:客户端和服务器端同在一个项目中

同一项目下多个可执行文件&#xff0c;多个子项目参照以下&#xff1a; 一、项目目录 项目/|-- client/|-- main.rs|-- Cargo.toml|-- server/|-- main.rs|-- Cargo.toml|-- Cargo.toml二、项目公共 Cargo.toml [workspace] # 定义Rust工作区配置 members …

mac本地安装mysql

本人环境 macOs 14.5 1.下载安装mysql https://dev.mysql.com/downloads/mysql/ 配置环境变量&#xff0c;打开terminal vim ~/.bash_profile 添加MYSQL_HOME/usr/local/mysql 在PATH中添加 通过mysql --version命令查看版本 2.开启mysql 打开终端teminal,输入命令 sudo…

面试前端遇到的问题

面试官让我写一个delay函数然后这是我写的代码async function delay(){setTimeout(function() {}, 3000); }面试官就和我说不是这个&#xff0c;用promise当时就蒙了&#xff0c;什么东西&#xff0c;为什么要用promise然后问豆包说Promise 是 JavaScript 中用于处理异步操作的…

Ubuntu Desktop 22.04.5 LTS 使用默认的 VNC 远程桌面

1. 打开 VNC 打开设置 - 分享 - 远程桌面2. 配置 VNC 打开远程桌面 启用vnc 选择vnc密码访问 配置密码3. 固定密码 远程桌面的访问密码在每次开机后会刷新一次&#xff0c;可以通过以下方式固定 打开【应用程序】&#xff0d;【附件】&#xff0d;密码和加密密钥&#xff08;或…

【无线安全实验4】基于伪随机数的WPS PIN码逆向(精灵尘埃/仙尘攻击)

文章目录1 原理分析1.1 WPS连接过程1.1.1 初始阶段1.1.2 注册阶段1.2 WPS攻击原理1.2.1 在线攻击1.2.2 离线攻击1.2.2.1 Ralink模式1.2.2.2 eCos模式2 实验过程3 参考资料在2011年 Stefan Viehbck 演示过WPS的在线暴力攻击&#xff0c;由于PIN码猜测最多只需11000种组合&#x…

IDEA开发过程中经常使用到的快捷键

IntelliJ IDEA 开发 Java 时常用的快捷键列表 代码编辑与行操作快捷键功能描述Ctrl Y删除当前行。Ctrl D复制当前行到下一行。Shift Alt ↑将当前行&#xff08;或选中块&#xff09;向上移动。Shift Alt ↓将当前行&#xff08;或选中块&#xff09;向下移动。Ctrl /注…

ubuntu使用webrtc库开发一个webrtc推拉流程序

目录 一. 前言 二. 整体交互流程 三. 类实现说明 1. WebRtcClient 2. SignalPeerClient 3. WebRTCStream 4. 视频源类 5. 拉流渲染 四. 使用示例 1. 推流代码示例 2. 拉流代码示例 一. 前言 在 《ubuntu编译webrtc库》我们介绍了如何在 ubuntu 上使用 webrtc 源代码…