一、線程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不需要構造文件描述符的表,只需要從用戶空間拷貝到內核空間一次。 |
結構 | 數組 | 數組 | 紅黑樹+就緒鏈表 |
開發復雜度 | 低 | 低 | 中 |