1.? 輸入系統應用編程
1.1 輸入系統介紹
? ? ? ? 常見的輸入設備有鍵盤、鼠標、遙控桿、書寫板、觸摸屏等。用戶經過這些輸入設備與Linux系統進行數據交換。這些設備種類繁多,如何去統一它們的接口,Linux為了統一管理這些輸入設備實現了一套能兼容所有輸入設備的框架叫做輸入系統。
1.1 輸入系統框架
? ? ? ? 輸入框架主要由輸入系統的驅動層,輸入系統核心層,輸入系統時間層以及用戶空間tslib, libinput等組成。
? ? ? ? 輸入系統驅動層:負責從硬件處獲取數據,轉換為標準的輸入事件,常見的硬件有鼠標、鍵盤、畫圖板和觸摸屏等。
? ? ? ? 輸入系統核心層:接收硬件驅動層的輸入,將硬件獲取的數據轉換為統一的格式并發送給系統事件層。
? ? ? ? 輸入系統事件層:用于給用戶空間提供接口
? ? ? ? 用戶空間APP: APP可以直接打開驅動節點訪問,也可以通過庫tslib或libinput等使用輸入設備。
? ? ? ? 數據的流程
1?APP發起讀操作,若無數據則休眠;
2. 用戶操作設備,硬件上產生中斷
3. 輸入系統驅動層對應的驅動程序處理中斷:
? ? ? ? 讀取到數據,轉為標準的輸入事件,并向核心層匯報
4. 輸入系統核心層決定將輸入事件轉發給哪個handler處理。
5. 時間層的handler接收到事件后根據誰調用了他就喚醒哪個APP,對應的APP就可以返回數據了。
?6.?APP 獲 得 數 據 的 方 法 有 2 種 :
? ? ? ? 直 接 訪 問 設 備 節 點 比如/dev/input/event0,1,2,...
????????通過 tslib、libinput 這類庫來間接訪問設備節點。這些庫簡化了對數據的處理。
????????
1.2 編寫APP需要掌握的知識
? ? ? ? ?APP得到的一些列輸入事件本質是一個個"struct input_event",它的定義如下:
? ? ? ? ? ?每個輸入事件都含有timeval結構體,表示的是“自系統啟動以來過了多少時間”, 它是一個結構體,含有“tv_sec、tv_usec"兩項(即秒、微秒)。?
? ????????驅動程序上報數據含義的三項重要內容:?
? ? ? ? type: 代表類, 比如EV_KEY,按鍵類
? ? ? ? code: 哪個, 比如KEY_A
? ? ? ? value: 值,比如0-按下,1-松開
?1.3 調試技巧
? ?1.確定設備信息
? ? ? ? 輸入設備的設備節點名為/dev/input/eventX(也可能是/dev/eventX, X表示0、1、2等數字)
ls /dev/input/* -l
或
ls /dev/event* -l
可以看到下面的圖? ? ?
? ?如何知道這些設備節點對應什么硬件呢?
cat /proc/bus/input/devices
1. I: 設備ID
2. N: 設備名稱
3. P: 系統層次結構中設備的物理路徑
4. S:位于sys文件系統的路徑
5. U:設備的唯一標識碼
6. H: 與設備關聯的輸入句柄列表
7. B:位圖, 如B: EV=B? 用來表示該設備支持那類輸入事件, B: ABS=2568000 3表示該設備支持EV_ABS這一類事件的哪些事件。
2. 使用命令讀取數據
????????調試輸入系統時,直接執行類似下面的命令,然后操作對應的輸入設備即可讀出數據:
hexdump /dev/input/event0
3 APP訪問硬件的四種方式
?查詢方式:
? ? ? ? APP調用open函數時,傳入"O_NONBLOCK"表示“非阻塞”, APP調用read函數讀取數據的時候,如果驅動程序中有數據,那么APP的read函數會返回數據否則會立即返回錯誤。
休眠-喚醒方式:
? ? ? ? APP調用open函數的時候,不傳入"O_NONBLOCK"。
? ? ? ? APP調用read時,如果驅動中有數據,那么read會返回數據,否則APP就在內核態休眠,當有數據時驅動會把APP喚醒,read函數恢復指向并返回數據給APP。
POLL/SELECT方式
????????POLL 機制、SELECT 機制是完全一樣的,只是 APP 接口函數不一樣。他們就是定一個鬧鐘,在調用poll、select函數可以傳入“超時時間”。在這段時間內,條件合適時(比如有數據可讀、有空間可寫)就會立刻返回,否則等到“超時時間”結束時返回錯誤。調用poll或select后如果有人操作了硬件,驅動程序獲得數據后就會把APP喚醒,導致poll或select返回,如果在超時時間內無人操作硬件,則 時間到后 poll 或 select 函數也會返回。APP 可以根據函數的返回值判斷返回 原因:有數據?無數據超時返回?
14 int main(int argc, char **argv)
15 {
16 int fd;
26 struct pollfd fds[1];
……
61 fd = open(argv[1], O_RDWR | O_NONBLOCK);
……
94 while (1)
95 {
96 fds[0].fd = fd;
97 fds[0].events = POLLIN;
98 fds[0].revents = 0;
99 ret = poll(fds, nfds, 5000);
100 if (ret > 0)
101 {
102 if (fds[0].revents == POLLIN)
103 {
104 while (read(fd, &event, sizeof(event)) == sizeof(even
t))
105 {
106 printf("get event: type = 0x%x, code = 0x%x, val
ue = 0x%x\n", event.type, event.code, event.value);
107 }
108 }
109 }
110 else if (ret == 0)
111 {
112 printf("time out\n");
113 }
114 else
115 {
116 printf("poll err\n");
117 }
118 }
119
120 return 0;
121 }
122
步驟:
? ? ? ? 1. APP先調用open函數
? ? ? ? 2. 設置pollfd結構體
? ? ? ? 3. 設置查詢的文件
? ? ? ? 4. 設置查詢的事件
? ? ? ? 5. 清除“返回的事件”
? ? ? ? 6. 使用poll函數查詢事件,指定超時時間
? ? ? ? 7. APP根據poll的返回值判斷有數據之后,就調用read函數讀取數據。
異步通知方式
????????所謂異步通知,就是 APP 可以忙自己的事,當驅動程序用數據時它會主動給APP 發信號,這會導致 APP 執行信號處理函數。
? ? ? ? 驅動程序通知APP時,它會發出“SIGIO”這個信號,表示有“IO事件”要處理。而APP要想處理SIFIO信息需要提供信號處理函數,并且要跟SIGIO掛鉤,可以通過一個signal函數來“給某個信號注冊處理函數”用法如下:
#include <signal.h>
typedef void (*sighandler_t)(int); // 1.編寫函數
sighandler_t signal(int signum, sighandler_t); // 2. 注冊
? ? ? ? 內核里有那么多驅動,你想讓哪一個驅動給你發SIGIO信號?
? ? ? ? APP首先要打開驅動程序的設備節點,然后把進程ID高速驅動程序,并使用FASYNC位為1使能異步通知。????????
// 編寫驅動程序
static void sig_func(int sig)
{
int val;
read(fd, &val, 4);
printf("get button : 0x%x\n", val);
}
// 注冊信號處理函數
signal(SIGIO, sig_func)
// 打開驅動
fd = open(argv[1], O_RDWR);
//把進程ID告訴驅動
fcntl(fd, F_SETOWN, getpid());
// 使能驅動的FASYNC功能
flags = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, flags | FASYNC);
2. 網絡編程
2.1 網絡通信基礎知識
? ? ? ? 大部分的網絡應用系統可以分為兩個部分:客戶和服務器,網絡服務程序架構有CS模式或者BS模式。
? ? ? ? CS即Client/Server(客戶機/服務器)結構:C/S結構的主要特點是交互性強、具有安全的存取模式、網絡通信量低、響應速度快、利于處理大量數據。該結構的程序是針對性開發,變更不夠靈活,維護和管理的難度較大。并且,由于該結構的每臺客戶機都需要安裝相應的客戶端程序,分布功能弱且兼容性差,不能實現快速部署安裝和配置,因此缺少通用性,具有較大的局限性。
? ? ? ? BS即Browser/Server(瀏覽器/服務器)結構:只安裝維護一個服務器(Server),客戶端采用**瀏覽器**運行軟件。B/S結構應用程序相對于傳統的C/S結構應用程序是一個非常大的進步。 B/S結構的主要特點是分布性強、維護方便、開發簡單且共享性強、總體擁有成本低。但數據安全性問題、對服務器要求過高、數據傳輸速度慢、軟件的個性化特點明顯降低,這些缺點是有目共睹的,難以實現傳統模式下的特殊功能要求。
OSI七層網絡協議
? ? ? ? 七層網絡參考模型(OSI參考模型)是國際標準化組織指定的一個用于計算機或通信系統間互聯的標準體系。
? ? ? ??
- ? 物理層:定義物理設備標準,如網線的接口類型、光纖的接口類型、各種傳輸介質的傳輸速率等。它的主要作用是傳輸比特流。(數據為比特)
- 數據鏈路層:建立邏輯連接、進行硬件地址尋址、差錯校驗等功能。定義了如何讓格式化數據以幀為單位進行傳輸,以及如何讓控制對物理介質的訪問。
- 網絡層:進行邏輯地址尋址,在位于不同地理位置的網絡中的兩個主機系統之間提供連接和路徑選擇。Internet的發展使得從世界各站點訪問信息的用戶數大大增加,而網絡層正是管理這種連接的層。
- 傳輸層:定義了一些傳輸數據的協議和端口號( WWW 端口 80 等),如:TCP(傳輸控制協議,傳輸效率低,可靠性強,用于傳輸可靠性要求高,數據量大的數據),UDP(用戶數據報協議,與TCP 特性恰恰相反,用于傳輸可靠性要求不高,數據量小的數據,如 QQ 聊天數據就是通過這種方式傳輸的)。 主要是將從下層接收的數據進行分段和傳輸,到達目的地址后再進行重組。常常把這一層數據叫做段。
- 會話層:通過傳輸層(端口號:傳輸端口與接收端口)建立數據傳輸的通路。主要在你的系統之間發起會話或者接受會話請求。
- 表示層:數據的表示、安全、壓縮。主要是進行對接收的數據進行解釋、加密與解密、壓縮與解壓縮等。
- 應用層:網絡服務與最終用戶的一個接口。這一層為用戶的應用程序(例如電子郵件、文件傳輸和終端仿真)提供網絡服務。
TCP/IP四層模型
TCP/IP 協議在一定程度上參考了 OSI 的體系結構
- 應用層:它是體系結構中的最高層,直接為用戶的應用進程提供服務。在因特網中的應用層協議很多,如支持萬維網應用的 HTTP 協議,支持電子郵件的 SMTP 協議,支持文件傳送的 FTP 協議,DNS,POP3,SNMP,Telnet 等等。
- 運輸層:負責向兩個主機中進程之間的通信提供服務。主要使用以下兩種協議TCCP和UDP。
- 網絡層:負責將被稱為數據包(datagram)的網絡層分組從一臺主機移動到另一臺主機。
- 鏈路層(網卡層):因特網的網絡層通過源和目的地之間的一系列路由器路由數據報。
- 物理層(硬件層):在物理層上所傳數據的單位是比特。物理層的任務就是透明地傳送比特流。
2.2 TCP
???????TCP協議是面向連接的通信協議,即傳輸數據之前,在發送端和接收端建立邏輯連接,然后再傳輸數據,它提供了兩臺計算機之間可靠無差錯的數據傳輸。
- 32位序號:TCP將要傳輸的每個字節都進行了編號,序號是本報文段發送的數據組的第一個字節的編號,序號可以保證傳輸信息的有效性。比如:一個報文段的序號為300,此報文段數據部分共有100字節,則下一個報文段的序號為401。
- 32位確認序號:每一個ACK對應這一個確認號,它指明下一個期待收到的字節序號,表明該序號之前的所有數據已經正確無誤的收到。確認號只有當ACK標志為1時才有效。比如建立連接時,SYN報文的ACK標志位為0。
- 4位首部長度(數據偏移): 表示該TCP頭部有多少個32位bit(有多少個4字節),所以TCP頭部大長度是15 * 4 = 60。根據該部分可以將TCP報頭和有效載荷分離。TCP報文默認大小為20個字節。
- 6位標志位:
????????????????URG:它為了標志緊急指針是否有效。
????????????????ACK:標識確認號是否有效。
????????????????PSH:提示接收端應用程序立即將接收緩沖區的數據拿走。
????????????????RST:它是為了處理異常連接的, 告訴連接不一致的一方,我們的連接還沒有建立好,要求對方重新建立連接。我們把攜帶RST標識的稱為復位報文段。
????????????????SYN:請求建立連接; 我們把攜帶SYN標識的稱為同步報文段。
????????????????FIN:通知對方, 本端要關閉連接了, 我們稱攜帶FIN標識的為結束報文段。
TCP建立連接需要三次握手
?(1)Client首先向Server發送連接請求報文段,同步自己的seq(x),Client進入SYN_SENT狀態。
(2)Server收到Client的連接請求報文段,返回給Client自己的seq(y)以及ack(x+1),Server進入SYN_REVD狀態。
(3)Client收到Server的返回確認,再次向服務器發送確認報文段ack(y+1),這個報文段已經可以攜帶數據了。Client進入ESTABLISHED狀態。
(4)Server再次收到Client的確認信息后,進入ESTABLISHED狀態。
?
TCP斷開連接需要四次握手
(1)Client向Server發送斷開連接請求的報文段,seq=m(m為Client最后一次向Server發送報文段的最后一個字節序號加1),Client進入FIN-WAIT-1狀態。
(2)Server收到斷開報文段后,向Client發送確認報文段,seq=n(n為Server最后一次向Client發送報文段的最后一個字節序號加1),ack=m+1,Server進入CLOSE-WAIT狀態。此時這個TCP連接處于半開半閉狀態,Server發送數據的話,Client仍然可以接收到。
(3)Server向Client發送斷開確認報文段,seq=u(u為半開半閉狀態下Server最后一次向Client發送報文段的最后一個字節序號加1),ack=m+1,Server進入LAST-ACK狀態。
(4)Client收到Server的斷開確認報文段后,向Server發送確認斷開報文,seq=m+1,ack=u+1,Client進入TIME-WAIT狀態。
(5)Server收到Client的確認斷開報文,進入CLOSED狀態,斷開了TCP連接。?? ??? ??? ?
(6)Client在TIME-WAIT狀態等待一段時間(時間為2*MSL((Maximum Segment Life)),確認Client向Server發送的最后一次斷開確認到達(如果沒有到達,Server會重發步驟(3)中的斷開確認報文段給Client,告訴Client你的最后一次確認斷開沒有收到)。如果Client在TIME-WAIT過程中沒有再次收到Server的報文段,就進入CLOSES狀態。TCP連接至此斷開
2.3 UDP????????
????????用戶數據報協議(User Datagram Protocol)。UDP協議是一個面向無連接的協議。傳輸數據時,不需要建立連接,不管對方端服務是否啟動,直接將數據、數據源和目的地都封裝在數據包中,直接發送。每個數據包的大小限制在64k以內。它是不可靠協議,因為無連接,所以傳輸速度快,但是容易丟失數據。日常應用中,例如視頻會議、QQ聊天等。
- 16位UDP長度表示整個數據報(UDP首部+UDP數據)的長度
- 如果校驗和出錯,就會直接丟棄(UDP校驗首部和數據部分)
TCP和UDP的區別?
1. TCP是面向連接而UDP是無連接的
2. TCP是提供可靠的服務,保證無差錯無丟失,不重復且按序到達,UDP則是盡最大的努力進行交付,不保證可靠交付。
3. TCP是面向字節流的,而UDP是面向報文的,UDP沒有擁塞控制,因此網絡出現擁塞不會使源主機的發送速率降低,常用于實時通信。
4.?每一條的TCP只能是點到點的,UDP支持一對一,一對多,多對一和多對多的交互通信
5. 頭部開銷TCP位20字節,UDP為8字節
6.?TCP的邏輯通信的信道是全雙工的可靠信道,而UDP則是不可靠信道。
為何存在UDP協議?
????????既然 TCP 提供了可靠數據傳輸服務,而 UDP 不能提供,那么 TCP 是否總是首選呢?
答案是否定的,因為有許多應用更適合用 UDP ,舉個例子:視頻通話時,使用 UDP ,偶爾的丟包、偶爾的花屏時可以忍受的;如果使用 TCP ,每個數據包都要確保可靠傳輸,當它出錯時就重傳,這會導致后續的數據包被阻滯,視頻效果反而不好。所以需要傳輸時效好的時候應該采用UDP。
2.4?網絡編程的主要函數
(1) socket()函數: 用于創建套接字, 同時指定協議和類型。
#include <sys/types.h>
#include <sys/socket.h>//需要引入的頭文件
int socket(int domain, int type, int protocol);
? 參數:
? ? ? ? domain: 指明所使用的協議族,通常為AF_INET,表示互聯網協議族(TCP/IP協議族)
? ? ? ? type: 參數指定socket的類型。
? ? ? ? ? ? ? ? SOCK_STREAM: 使用TCP。
? ? ? ? ? ? ? ? SOCK_DGRAM: 使用UDP。
? ? ? ? ? ? ? ? SOCK_RAW: 允許程序使用底層協議原始套接字允許對底層協議如IP或ICMP
????????????????????????????????????進行直接訪問,功能強大但使用較為不便,主要用于一些協議的開發。
? ? ? ? protocol: 通常賦值為“0”, 代表選擇type類型對應的默認協議
? ?返回值:
? ? ? ? 成功返回非負套接字描述符,失敗返回-1?
(2) bind()函數: 用于綁定IP地址和端口號到socket
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
參數:
? ? ? ? sockfd: 是一個socket描述符。
? ? ? ? addr:是一個指向包含有本機IP地址及端口號等信息的sockaddr類型的指針,指向要綁
???????????????????定給sockfd的協議地址結構。這個輸入可以在#include <sys/socket.h>里的
? ? ? ? ? ? ? ? ? ? sockaddr所賦值,但是他的sa_data把目標地址和端口信息混合了,可以將其
? ? ? ? ? ? ? ? ? ? 變成#include<netinet/in.h>或#include <arpa/inet.h>中定義的sockaddr_in結構體
? ? ? ? ? ? ? ? ? ? ?該結構體解決了sockaddr的缺陷,把port和addr 分開儲存在兩個變量中,可以
? ? ? ? ? ? ? ? ? ? ?可以將其賦值后然后強制類型轉換為sockaddr。
? ? ? ? addrlen: 地址的長度,一般用于sizeof(struct sockaddr_in)表示。
?返回值:
????????成功返回0, 失敗返回-1.
(3) listen()函數? 監聽被綁定的端口,維護兩個隊列,一個是未完成連接隊列,等待完成TCP三次握手(SYN_REVD),另一個是已完成連接隊列,表示已完成三次握手的客戶端,這些套接字處于ESTABLISHED狀態。
int listen(int sockfd, int backlog);
參數:
? ? ? ? sockfd:socket系統調用返回的服務端socket描述符
? ? ? ? backblog:指定在請求隊列 中允許的最大請求數,大多數系統默認為5.
返回值:
????????成功返回0, 失敗返回1
(4) accept()函數,該函數由TCP服務器調用,用于從已完成連接隊列對頭返回下一個已完成連接,如果已完成連接隊列為空,那么進程被投入睡眠。
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
參數:
? ? ? ? sockfd:是socket系統調用返回的服務器端socket描述符
? ? ? ? addr:用來返回已連接的對端(客戶端)的協議地址
? ? ? ? addrlen:客戶端地址長度,注意需要取地址
返回值:
????????accept 調用時,服務器端的程序會一直阻塞到有一個客戶程序發出了連接。 accept 成功時返回最后的服務器端的文件描述符,這個時候服務器端可以向該 描述符寫信息了,失敗時返回-1 。
(5)connect()函數, 該函數用于綁定之后的client端(客戶端),與服務器建立連接
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
參數:
? ? ? ? sockfd: 創建的socket描述符
? ? ? ? addr: 服務端的ip地址和端口號的地址結構指針
? ? ? ? addrlen:地址的長度,通常被設置為sizeof(struct sockaddr)
返回值:成功返回0,遇到錯誤時返回-1,并且errno中包含相應的錯誤碼。
(6) send()、recv() 函數只能對于連接狀態函數只能對處于連接狀態的套接字進行使用,參數sockfd為已建立好連接的套接字描述符, 這是用于TCP發送和接收信息。
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
參數:
? ? ? ? sockfd: 為已建立好連接的套接字描述符即accept函數的返回值
? ? ? ? buf: 要發送的內容
? ? ? ? len:發送內容的長度
? ? ? ? flags:設置為MSG_DONTWAITMSG時表示非阻塞,設置為0時,功能和write一樣。
返回值:
? ? ? ? 成功返回實際發送的字節數,失敗返回-1.
ssize_t recv(int sockfd, const void *buf, size_t len, int flags);
參數:
? ? ? ? sockfd: 在哪個套接字接
? ? ? ? buf:存放要接收的數據的首地址
? ? ? ? len:要接收的數據的字節
? ? ? ? flags:設置為
MSG_DONTWAITMSG
?時 表示非阻塞,設置為0時 功能和read一樣返回值:成功返回實際發送的字節數失敗返回-1.
(7)?sendto()、recvfrom()函數(UDP發送、接收消息)
int sendto(int s, const void *buf, int len, unsigned int flags, const struct sockaddr *to, int tolen);
參數:
? ? ? ? s:socket描述符
? ? ? ? buf: UDP數據報緩沖區
? ? ? ? len:UDP數據報的長度
????????flags:調用方式標志位(一般設置為0),設置為
MSG_DONTWAITMSG
?時 表示非阻塞
????????to:指向接收數據的主機地址信息的結構體(sockaddr_in需類型轉換);
????????tolen:to所指結構體的長度;返回值:成功則返回實際傳送出去的字符數,失敗返回-1,錯誤原因會存于errno 中。
int recvfrom(int s, void *buf, int len, unsigned int flags,struct sockaddr *from, int *fromlen);
參數:????????
????????s: socket描述符;
????????buf: UDP數據報緩存區(包含所接收的數據);
????????len: 緩沖區長度。
????????flags: 調用操作方式(一般設置為0),設置為MSG_DONTWAITMSG 時 表示非阻塞
????????from: 指向發送數據的客戶端地址信息的結構體(sockaddr_in需類型轉換);
????????fromlen:指針,指向from結構體長度值。返回值:成功則返回實際接收到的字符數,失敗返回-1,錯誤原因會存于errno 中。
2.5 TCP/UDP的socket通信流程


網絡字節序的定義:將收到的第一個字節的數據當做高位來看待,這就要求發送端的發送的第一個字節應該是高位。而在發送端發送數據時,發送的第一個字節是該數字在內存中起始地址對應的字節。可見多字節數值在發送前,在內存中數值應該以大端法存放。
uint16_t htons(uint16_t hostshort);//將16位主機字節序數據轉換成網絡字節序數據uint32_t htonl(uint32_t hostlong);//將32位主機字節序數據轉換成網絡字節序數據
將網絡字節序轉換主機字節序函數
uint16_t ntohs(uint16_t netshort);//將16位網絡字節序數據轉換成主機字節序數據
uint32_t ntohl(uint32_t netlong);//將32位網絡字節序數據轉換成主機字節序數據
2.6? TCP/UDP例程
服務器端
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <errno.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <unistd.h>
#include <string.h>
int main()
{int s_fd;//socket 返回的套接字,服務器端int c_fd;//accept函數返回的客戶端的套接字int ret;int c_len;//客戶端結構體的大小int readSize;int quit_flag = 0;pid_t pid;char readBuf[128] = {'\0'};//存放讀取的客戶端內容char writeBuf[128] = {'\0'};//存放發往客戶端的內容char ipBuf[32] = {'\0'};struct sockaddr_in s_addr;//設置本機ip地址及端口的結構體struct sockaddr_in c_addr;//接收的客戶端的ip地址及端口的結構體//1.創建套接字(socket)s_fd = socket(AF_INET,SOCK_STREAM, 0);if(s_fd<0){perror("creat socket fail");return -1;}s_addr.sin_family = AF_INET;s_addr.sin_port = htons(8989);//綁定端口號,并將端口號變為網絡字節序ret = inet_aton("192.168.109.137",&s_addr.sin_addr);//綁定本機IP地址,并將其轉換為二進制IP地址 if(ret == 0){perror("inet_aton fail");return -1;}//2.將socket與IP地址和端口綁定(bind)ret = bind(s_fd,(struct sockaddr*)&s_addr,sizeof(struct sockaddr_in));//注意將struct sockaddr_in*強制轉換為struct sockaddr*if(ret < 0){perror("bind fail");return -1;}//3.監聽被綁定的端口(listen)listen(s_fd,5);c_len = sizeof(struct sockaddr_in);while(1){//4.接收連接請求(accept)c_fd = accept(s_fd, (struct sockaddr*)&c_addr, &c_len);if(c_fd < 0){perror("accept error");}else{memset(ipBuf,'\0',32);strcpy(ipBuf,inet_ntoa(c_addr.sin_addr));printf("get connect:%s\n",ipBuf);//將客戶度鏈接的地址轉換為十進制打印pid = fork();//創建子進程對接每個客戶端if(pid == 0){pid_t pid;pid = fork();if(pid > 0){while(1){printf("message to the IP :%s:",ipBuf);memset(writeBuf,'\0',128);scanf("%s",writeBuf);write(c_fd,writeBuf,strlen(writeBuf));}}else if(pid == 0){ while(1){//從socket中讀取客戶端發送來的信息(read)進程memset(readBuf,'\0',sizeof(readBuf));readSize = read(c_fd,readBuf,128);if(readSize < 0){perror("read fail");}else if(readSize == 0){printf("client quits\n");break;}else{printf("IP %s :%s\n",inet_ntoa(c_addr.sin_addr),readBuf);}}//發送信息到客戶端} }}}//7.關閉socket(close)close(c_fd);close(s_fd);return 0;}
客戶端
#include <signal.h>
#include <sys/types.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <errno.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
int main(int argc,char *argv[])
{int c_fd;//客戶端套接字int ret;int readSize;pid_t pid;char readBuf[128] = {'\0'};//存放讀取的服務器內容char writeBuf[128] = {'\0'};//存放發往服務器的內容struct sockaddr_in addr;//想要連接的目標地址if(argc < 3){printf("the param is not good! Please in put ip and port\n");exit(-1);}//1.創建套接字(socket)c_fd = socket(AF_INET,SOCK_STREAM,0);if(c_fd<0){perror("creat socket fail");return -1;}addr.sin_family = AF_INET;addr.sin_port = htons(atoi(argv[2]));//設置目標端口inet_aton(argv[1],&addr.sin_addr);//設置目標IP地址//2.連接指定計算機的端口(connect)ret = connect(c_fd,(struct sockaddr*)&addr,sizeof( struct sockaddr_in));if(ret<0 ){perror("connect error");return -1;}pid = fork();if(pid > 0){while(1){//3.從socket中讀取服務端發送過來的消息(read)memset(readBuf,0,sizeof(readBuf));readSize = read(c_fd,readBuf,128);if(readSize == -1){perror("read");}printf("get %d from the sever:%s\n",readSize,readBuf);}}else if(pid ==0){while(1){//4.向服務區中發送數據printf("Please input:");memset(writeBuf,'\0',128);scanf("%s",writeBuf);write(c_fd,writeBuf,strlen(writeBuf));if(strstr(writeBuf,"quit")!=NULL){kill(getppid(),9); kill(getpid(),9);}}}return 0;
}
3 進程
3.1 進程簡介
概念:程序的一個執行實例,正在執行的程序。
進程=對應的代碼和數據+進程對于的PCB控制塊
內核觀點:擔當分配系統資源(CPU時間,內存的實體)
? ? ? ? 當我們雙擊可執行程序運行的時候本質上是將這個程序加載到內存中,然后CPU對其進行逐行的語句執行,一旦程序加載到內存后,嚴格意義上應該將其稱為進程。
描述進程-PCB
? ? ? ? 操作系統如何獲得進程信息?
? ? ? ? 進程信息被放在一個叫做進程控制塊的數據結構中,可以理解為進程屬性的集合,簡稱為PCB。操作系統將這些PCB以雙鏈表的形式組織在一起,那么操作系統只要拿到雙鏈表的頭指針便可以訪問到所有的PCB。
Linux中描述PCB的結構體
? ? ? ? 由于Linux是用C語言進行編寫的,那么Linux中的進程控制塊必定是用結構體來實現的。Linux中的PCB叫做task_struct,當進程創建的時候它會被裝載到RAM內存里。
task_struct的內容
標示符(pid):描述本進程的唯一標識符,用來區別其他進程
狀態:任務狀態
優先級:相對于其他進程的優先級
程序計數器: 程序中即將被執行的下一條指令的地址
內存指針:包括程序代碼和進程相關數據的指針,還有和其他進程共享的內存塊指針
上下文數據:進程執行時處理器的寄存器中的數據
I/O狀態信息:包括顯示的I/O請求,分配給進程的I/O設備和被進程使用的文件列表
記賬信息:可能包括處理器時間總和,使用的時鐘數總和,時間限制,記賬號等
? ? ? ? 上下文數據的理解:上下文數據是指具體的上下文信息,它包括了程序或者系統的狀態、寄存器值、內存映射、文件描述符、信號處理器等具體數據。CPU在怕跑一個進程時,沒有跑完就開始切換其他進程,為了下次繼續跑完這個進程,會保留這個進程的上下文數據,當這個進程回來時,會把上下文數據移動到CPU內部繼續執行。
Linux中如何查看進程
? ? ? ? 1. 在根目錄下有一個名為proc的系統文件夾,文件夾中包含大量的進程信息,其中有些子目錄的目錄名為數字,這些數字其實就是某一進程的PIC,如果要查看對應進程的信息直接ls /proc/<pid>? 即可。
? ? ? ? 2. 通過ps命令查看, 集合grep搜索可以只顯示需要查看的進程的信息。
ps aux
Linux如何獲取標識符
#include <stdio.h>
#include <unistd.h>
int main()
{while(1){printf("PID: %d, PPID: %d\n", getpid(), getppid());sleep(1);}return 0;
}
? ? ? ? getpid()是獲取當前進程的標識符,getppid() 是獲取父進程的標識符,這兩個都是系統調用函數。
Linux如何創建子進程
? ? ? ? fork是使用系統調用級別的函數創建一個子進程。
fork函數的返回值:
? ? ? ? 1. 如果子進程創建成功,在父進程中返回子進程的PID, 而在子進程中返回0
? ? ? ? 2. 如果子進程創建失敗,則在父進程中返回-1
注意: fork函數被調用之前的代碼被父進程執行,而fork函數之后的代碼,則默認情況下父子進程都可以執行,父子進程雖然代碼共享,但是父子進程的數據各自開辟空間。
fork函數到底做了什么?
? ? ? ? fork函數創建子進程并不是在內存中重新拷貝一份代碼和數據,而是在內存中以父進程為模板創建一個新的PCB結構,其實父子進程是PCB不一樣,但是他們指向的代碼和數據是同一份。
fork函數為什么能夠實現父子進程代碼和數據的獨立性?
??????寫時拷貝(copy-on-write, COW)就是等到修改數據時才真正分配內存空間,這是對程序性能的優化,可以延遲甚至是避免內存拷貝,當然目的就是避免不必要的內存拷貝。維護一個開辟的4個字節的空間,用來記錄有多少個指針指向這片空間。? 當我們父子進程嘗試去修改數值的值時,便會觸發寫時拷貝,寫時拷貝會給我們復制一份當前數據的值,讓我們的父子進程去其他位置修改數據。
fork為什么有兩個返回值?
?????????fork 系統調用函數在執行 return 語句之前,子進程就已經創建完成甚至已經被操作系統調度了,所以當執行 return 語句返回結果的時候,OS自動觸發寫時拷貝,分別把結果傳入兩者的備份空間中。
3.2?進程的狀態
?理論上的進程狀態
創建狀態:?當一個進程被創建時,它處于創建狀態。在這個階段,操作系統為進程分配必要的資源(將代碼和數據拷貝到內存,創建PCB結構體等),并為其分配一個唯一的進程標識符(PID)。
就緒狀態:進程就緒狀態是指進程已經滿足了運行的條件,進程PCB被調度到CPU運行隊列中,排隊等待系統分配CPU資源來執行的狀態。
運行狀態:進程PCB被調度到CPU運行隊列中且已被分配CPU資源,就叫做運行態。在這個階段,進程的指令會被執行,它可以訪問CPU和其他系統資源。只有運行狀態下的進程才能占用CPU資源。
阻塞狀態:當一個進程無法繼續執行,因為它需要等待某些非CPU資源就緒時,它會進入阻塞狀態。這些事件可能包括等待用戶輸入、等待磁盤I/O操作完成等。在阻塞狀態下,進程會被調度到阻塞隊列,不會占用CPU資源。
掛起狀態:當內存不足時,如果一個進程長時間不執行或者處于低優先級狀態,操作系統可能會將其代碼和數據置換出內存并存儲到磁盤上的swap分區中。其PCB(進程控制塊)仍然存在于進程表中。
終止狀態:當進程完成其任務或被操作系統終止時,它進入終止狀態。在這個階段,進程可以釋放所有已分配資源,并從系統中移除。
進程轉換流程:
?
3.3 Linux進程狀態
R狀態:進程被調度到CPU運行隊列中,分配到CPU資源的進程是運行態,沒有分配到的是就緒態。
S狀態:可中斷睡眠狀態(阻塞、掛起),即睡眠狀態可以通過發送信號等方式被動喚醒,或由操作系統直接中斷。
D狀態:磁盤睡眠狀態(IO阻塞、IO掛起),不可被動喚醒,不可被中斷
T狀態:信號暫停狀態,表示進程被暫停,并且可以通過信號來暫停或恢復進程的執行:-19暫停,-18繼續。
t狀態:調試暫停狀態,表示進程被調試器(如gdb)跟蹤調試,并且暫停了進程的執行。通常是由于調試器設置了斷點或執行了單步調試操作而進入的狀態。
Z狀態:僵尸狀態(終止),是指一個已經終止的子進程,但其父進程尚未獲取子進程的退出狀態。僵尸進程不會占用系統資源,因為它們已經終止并釋放了大部分資源。僵尸進程只在進程表中保留一條記錄,以便父進程在需要時獲取子進程的退出狀態。
X狀態:終止狀態,瞬時性非常強。
?
深度睡眠和淺度睡眠
? ? ? ? S狀態標識可中斷睡眠狀態,它表示進程正在等待某個事件的發生,例如等待I/O操作完成、等待信號量等,在這種狀態下,進程是可以被調度的。
????????D狀態標識不可中斷睡眠狀態,標識通常情況下D狀態是由于進程等待磁盤I/O操作完成而引起的,在這種狀態下,進程是不可被調度的,即操作系統無法將其切換到其他任務上執行。
? ? ? ? example:?當服務器壓力過大時,OS會通過一定的手段終止一些進程,起到節省資源的作用,此時如果為S狀態會被OS殺掉從而導致數據丟失,所以,為了防止進程異常終止而造成的數據丟失等問題的出現,該進程會被設置D狀態。D狀態下的進程不能被OS終止,只能等該進程得到磁盤讀寫結果后自動喚醒。
僵尸進程(Z)和孤兒進程
? ? ? ? 僵尸進程是子進程退出父進程還在運行,但是父進程沒有讀取到子進程的退出狀態,子進程就會進入Z狀態。
? ? ? ? 孤兒進程是父進程提前退出,但是此時未退出的子進程就會被稱為孤兒進程,孤兒進程會被1號init進程領養,需要init進程回收。
???????
僵尸進程的危害:
????????1. 僵尸進程的PCB不能回收:因為僵尸進程的退出狀態需要被維護下去,維護退出狀態需要數據維護。
? ? ? ? 2. 內存泄露:由于PCB不能及時回收會造成內存泄漏。
? ? ? ? 3. 創建進程有限:用戶創建的進程是有限的,僵尸進程過多會導致創建新的子進程失敗等諸多問題。
? ? ? ? 4. 需要及時回收:僵尸進程本身不會占用系統資源,但是會占用一定的進程表項和內核資源。
孤兒進程需要注意的地方:?子進程變為孤兒進程后,會從前臺進程轉為后臺,所以此時的
Ctrl c
無法終止子進程。
3.4 進程地址空間
?
進程地址空間: 進程地址空間本質是內存中的一種內核數據結構,在Linux中式由mm_struct實現的。在32位系統中,一個進程通常會被分配4GB的虛擬內存空間,地址范圍為0x00000000~0xFFFFFFFF。這4GB中大約有1GB的內核空間會被操作系統所保留,用于存儲操作系統本身的代碼和數據,剩下的3GB空間才是該進程的用戶空間。
?
特點:
? ? ? ? 1. 操作系統為每個進程都分配一個地址空間和對應的映射頁表
? ? ? ? 2. Linux內核中的地址空間本質上是一個mm_struct的結構,其中就包括了地址空間的區域劃分及其他屬性。
?
? ? ? ? 3. 地址空間中數據區域的變化,實際上是對區域的邊界start或end值進行調整。
? ? ? ? 4. task_struct中記錄了指向mm_struct結構體指針struct mm_struct *mm, *active_mm, 可以通過PCB找到進程對應的地址空間。
為什么要有進程地址空間?
1. 防止地址隨意被訪問,保護物理內存及其他進程
?
2. 將進程管理和內存管理進行解耦合,保證了進程獨立性的特點,每個進程都需要有獨立的進程地址空間及頁表,一個進程數據的改變并不回影響另一個進程。
3. 使得進程的內存管理分布有序化。
?
????????
4 多線程編程
4.1 線程簡介
線程概念
????????所謂線程就是操作系統所能調度的最小單位。普通的進程,只有一個線程在執行對應的邏輯。相比多進程編程而言,線程享有共享資源,即在進程中出現的全局變量, 每個線程都可以去訪問它,與進程共享“4G”內存空間,使得系統資源消耗減少。
?
? ? ? ? Linux中并沒有單獨地為線程創建相應地結構去管理,而是復用了進程的結構, 每一個線程都有自己的task_struct結構體,同時他們指向相同的虛擬空間mm_struct.
Linux下并不存在真正的多線程而是用進程模擬的!
1.嚴格上來說是沒有的,Linux是用進程PCB來模擬線程的,是一種完全屬于自己的一套線程方案。
2.站在CPU的視角,每一個PCB,都可以稱為輕量級進程。
3.Linux線程是CPU調度的基本單位,而進程是承擔分配系統資源的基本單位
4.進程用來整體申請資源,線程用來伸手向進程要資源
5.Linux中沒有真正意義的線程。通過進程模擬。
6.進程模擬線程的好處:PCB模擬線程,為PCB編寫的結構與算法都能進行復用,不用單獨為線程創建調度算法,降低維護成本,復用進程的那一套.可靠高效
線程的優點
????????創建一個新線程的代價要比創建一個新進程小得多
????????與進程之間的切換相比,線程之間的切換需要操作系統做的工作要少很多
????????進程間切換,需要切換頁表、虛擬空間、切換PCB、切換上下文
????????線程間切換,線程都指向同一個地址空間,頁表和虛擬地址空間就不需要切換了,只需要切換PCB和上下文,成本較低
????????少的多體現在,線程切換不需要更新太多cache(存儲大量經常使用的數據),進程切換要全部更新
線程的缺點
1. 性能損失:一個很少被外部事件阻塞的計算密集型線程往往無法與共它線程共享同一個處理器。如果計算密集型線程的數量比可用的處理器多,那么可能會有較大的性能損失,這里的性能損失指的是增加了額外的同步和調度開銷,而可用的資源不變。
2. 健壯性降低:編寫多線程需要更全面更深入的考慮,在一個多線程程序里,因時間分配上的細微偏差或者因共享了不該共享的變量而造成不良影響的可能性是很大的,換句話說線程之間是缺乏保護的,一個線程崩可能影響另一個線程。
3.??缺乏訪問控制:進程是訪問控制的基本粒度,在一個線程中調用某些OS函數會對整個進程造成影響。?
4.??編程難度提高:編寫與調試一個多線程程序比單線程程序困難。
線程的異常
單個線程如果出現除零,野指針問題導致線程崩潰,
進程
也會隨著崩潰
線程是進程的執行分支,線程出異常,就類似進程出異常,進而觸發信號機制,終止進程,進程終止,該進程內的所有線程也就隨即退出
4.2 Linux線程和進程
????????進程是承擔分配系統資源的基本實體,線程是調度的基本單位
線程共享進程數據,但也擁有自己的一部分數據:
線程ID、一組寄存器(存儲每個線程的上下文信息)、棧(線程的臨時數據)、errno、信號屏蔽字、調度優先級
Linux進程線程的父子控制的關系
????????進程不對任何子進程進行控制,進程的線程可以對同一進程的其他子進程加以控制。子進程不能對父進程施加控制,進程中所有線程都可以對主線程施加控制。
Linux 進程和線程的通信
父進程和子進程之間使用進程間通信機制,同一進程的線程通過讀取和寫入數據到進程變量來進行通信。
4.3 Linux線程控制
1. 線程的標識pthread_t
? ? ? ? 每一個進程都有一個唯一對應的PID號表示該進程,而線程的標識為tid號,本質是一個pthread_t類型的變量。線程號與進程號是表示線程和進程的唯一標識,但是對于線程號而言,其僅僅在其所屬的進程上下文中才有意義。
2. 獲取線程號
#include <pthread.h>
pthread_t pthread_self(void);
成功:返回線程號
3. 線程的創建
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routi
ne) (void *), void *arg);
功能:創建一個新的線程
thread:線程ID
attr:設置線程的屬性,attr為NULL表示使用默認屬性start_routine:是個函數地址,線程啟動后要執行的函數
arg:傳給線程啟動函數的參數
返回值:成功返回0;失敗返回錯誤碼

1.線程函數處進行return。
2.線程可以自己調用pthread_exit函數終止自己。
3.一個線程可以調用pthread_cancel函數終止同一進程中的另一個線程。
void* thread_run(void* args)
{while(true){cout << "new thread running,thread id: " << pthread_self() << endl;sleep(1);}return nullptr;
}
int main()
{pthread_t t;pthread_create(&t,nullptr,thread_run,nullptr);return 0;
}
? ? ? ??使用pthread_exit函數:?
using namespace std;void* thread_run(void* args)
{int cnt = 5;while(cnt--){cout << "new thread running,thread id: " << pthread_self() << endl;sleep(1);}pthread_exit((void*)2023);//將線程退出碼設為2023
}
int main()
{pthread_t t;pthread_create(&t,nullptr,thread_run,nullptr);void* ret = nullptr;pthread_join(t,&ret);//pthread_join表示線程等待,主線程執行完后還需等待其他線程//這里會把線程退出碼信息通過該函數給retcout << "new thread exit code is : " << (int64_t)ret << endl;//這里使用int64_t強制轉換是因為平臺下Linux的指針是8字節的。return 0;
}
????????pthread_cancel函數:線程是可以取消自己的,甚至新線程也可以取消主線程,取消成功的線程的退出碼一般是 -1
void* thread_run(void* args)
{int cnt = 5;while(cnt--){cout << "new thread running,thread id: " << pthread_self() << endl;sleep(1);}pthread_exit((void*)2023);
}
int main()
{pthread_t t;pthread_create(&t,nullptr,thread_run,nullptr);sleep(3);pthread_cancel(t);//取消新線程void* ret = nullptr;pthread_join(t,&ret);//pthread_join表示線程等待,主線程執行完后還需等待其他線程cout << "new thread exit code is : " << (int64_t)ret << endl;return 0;
}
4. 線程資源回收
阻塞方式
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
????????該函數為線程回收函數,默認狀態為阻塞狀態,直到成功回收線程后才返回。第一個參數為要回收線程的tid號,第二個參數為線程回收后接受線程傳出的數據。
非阻塞方式
#define _GNU_SOURCE
#include <pthread.h>
int pthread_tryjoin_np(pthread_t thread, void **retval);
該函數為非阻塞模式回收函數,通過返回值判斷是否回收掉線程,成功回收則返回 0 ,其余參數與 pthread_join 一致。
互斥鎖
信號量