一、线程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: 线程间使用 非0:1进程间使用)
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、阻塞式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;
}
必要性
- 避免进程在没有数据时无限制的阻塞;
- 规定时间未完成语句应有的功能,则会执行相关功能
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
特点:
- 监听的最大的文件描述符没有个数限制
- 异步IO,epoll当有事件产生被唤醒之后,文件描述符主动调用callback函数(回调函数)直接拿到唤醒的文件描述符,不需要轮询,效率高
- 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不需要构造文件描述符的表,只需要从用户空间拷贝到内核空间一次。 |
结构 | 数组 | 数组 | 红黑树+就绪链表 |
开发复杂度 | 低 | 低 | 中 |