1. 網路中進程之間如何通訊
進 程通訊的概念最初來自單機系統。由於每個進程都在自己的位址範圍內運行,為確保兩個相互通信的進
程之間既互不干擾又協調一致工作,操作系統為進程通信提供了相應設施,如
UNIX BSD有:管道(pipe)、命名管道(named pipe)軟中斷信號(signal)
UNIX system V有:消息(message)、共享存儲區(shared memory)和信號量(semaphore)等.
他們都僅限於用在本機進程之間通訊。網路進程通訊要解決的是不同主機進程間的相互通訊問題(可把同機進程通訊看成是其中的特例)。為此,首先要解決的是網間進程標識問題。同一主機上,不同進程可用進程號(process ID)唯一識別。但在網路環境下,各主機獨立分配的進程號不能唯一標識該進程。例如,主機A賦於某進程號5,在B機中也可以存在5號進程,因此,「5號進程」這句話就沒有意義了。 其次,作業系統支援的網路協定眾多,不同協定的工作方式不同,位址格式也不同。因此,網間進程通訊還要解決多重協定的辨識問題。
其實TCP/IP協定族已經幫我們解決了這個問題,網路層的「ip位址」可以唯一標識網路中的主機,而傳輸層的「協定+連接埠」可以唯一標識主機中的應用程式(進程)。這樣利用三元組(ip位址,協議,連接埠)就可以標識網路的進程了,網路中的進程通訊就可以利用這個標誌與其它進程進行互動。
使用TCP/IP協定的應用程式通常採用應用程式介面:UNIX BSD的套接字(socket)和UNIX System V的TLI(已經被淘汰),來實現網路進程之間的通訊。就目前而言,幾乎所有的應用程式都是採用socket,而現在又是網路時代,網路中進程通訊是無所不在,這就是我為什麼說「一切皆socket」。
2. 什麼是TCP/IP、UDP
TCP/IP(Transmission Control
TCP/IP(Transmission Control Protocol/Internet 它是為廣域網路(WANs)設計的。
TCP/IP協定存在於OS中,網路服務透過OS提供,在OS中增加支援TCP/IP的系統呼叫-Berkeley套接字,如Socket,Connect,Send,Recv等
UDP(User Data Protocol,用戶資料報協議)是與TCP相對應的協定。它是屬於TCP/IP協定族中的一種。如圖: TCP/IP協定族包含運輸層、網路層、連結層,而socket所在位置如圖,Socket是應用層與TCP/IP協定族通訊的中間軟體抽象層。 3. Socket是什麼 1、 socket套接字:
open –> 讀寫write/read –> 關閉close”模式來操作。 Socket就是這個模式的一個實現, socket即是一種特殊的文件,一些socket函數就是對其進行的操作(讀/寫IO、打開、關閉).
說白了Socket是應用層與TCP/IP協定協議通訊的中間軟體抽象層,它是一組介面。在設計模式中,Socket其實就是一個門面模式,它把複雜的TCP/IP協定族隱藏在Socket介面後面,對使用者來說,一組簡單的介面就是全部,讓Socket去組織數據,以符合指定的協定.
注意:其實socket也沒有層的概念,它只是一個facade設計模式的應用,讓程式設計變的更簡單。是一個軟體抽象層。在網路程式設計中,我們大量使用的都是透過socket實現的。
2、套接字描述詞
其實就是整數,我們最熟悉的句柄是0、1、2三個,0是標準輸入,1是標準輸出,2是標準誤差輸出。 0、1、2是整數表示的,對應的FILE *結構的表示就是stdin、stdout、stderr
套接字API最初是作為UNIX操作系統的一部分而開發的,所以套接字API與系統的其他I/O設備整合在一起。特別是,當應用程式要為因特網通訊而建立一個套接字(socket)時,作業系統就會傳回一個小整數作為描述符(descriptor)來識別這個套接字。然後,應用程式以該描述符作為傳遞參數,透過呼叫函數來完成某種操作(例如透過網路傳送資料或接收輸入的資料)。
在許多作業系統中,套接字描述符和其他I/O描述符是整合在一起的,所以應用程式可以對檔案進行套接字I/O或I/O讀取/寫入操作。
當應用程式要建立一個套接字時,作業系統就會回傳一個小整數作為描述符,應用程式則使用這個描述符來引用該套接字需要I/O請求的應用程式請求作業系統開啟一個文件。作業系統就建立一個檔案描述符提供給應用程式存取檔案。從應用程式的角度來看,檔案描述符是一個整數,應用程式可以用它來讀寫檔案。下圖顯示,作業系統如何把檔案描述符實作為一個指標數組,這些指標指向內部資料結構。
對於每個程式系統都有一張單獨的表格。精確地講,系統為每個運行的進程維護一張單獨的檔案描述符表。當進程開啟一個檔案時,系統會將一個指向此檔案內部資料結構的指標寫入檔案描述符表,並將該表的索引值傳回給呼叫者 。應用程式只需記住這個描述符,並在以後操作該檔案時使用它。作業系統把該描述符作為索引存取進程描述符表,透過指標找到保存該檔案所有的資訊的資料結構。
針對套接字的系統資料結構:
1)、套接字API裡面有個函數socket,它就是用來創造一個套接字。套接字設計的總體想法是,單一系統呼叫就可以創建任何套接字,因為套接字是相當籠統的。一旦套接字創建後,應用程式還需要呼叫其他函數來指定具體細節。例如呼叫socket將創建一個新的描述符條目:
2)、雖然套接字的內部資料結構包含很多字段,但是系統創建套接字後,大多數字字段沒有填寫。應用程式建立套接字後在該套接字可以使用之前,必須呼叫其他的過程來填入這些欄位。
3、檔案描述符和檔案指標的區別:
檔案描述符:在linux系統中開啟檔案就會獲得檔案描述符,它是個很小的正整數。每個進程在PCB(Process Control Block)中保存著一份檔案描述符表,而檔案描述符就是這個表的索引,每個表項都有一個指向已開啟檔案的指標。
檔案指標:C語言中使用檔案指標做為I/O的句柄。檔案指標指向進程使用者區中的一個稱為FILE結構的資料結構。 FILE結構包括一個緩衝區和一個檔案描述符。而文件描述符是文件描述符表的索引,因此從某種意義上說文件指標就是句柄的句柄(在Windows系統上,文件描述符被稱作文件句柄)。
4. 基本的SOCKET介面函數
在生活中,A要電話給B,A撥號,B聽到電話鈴聲後提起電話,這時A和B就建立起了連接,A和B就可以講話了。等交流結束,掛斷電話結束這次交談。 打電話很簡單解釋了這工作原理:「open—write/read—close」模式。
伺服器端先初始化Socket,再與連接埠綁定(bind),對連接埠進行監聽(listen),呼叫accept阻塞,等待客戶端連線。在這時如果有個客戶端初始化一個Socket,然後連接伺服器(connect),如果連線成功,這時客戶端與伺服器端的連線就建立了。客戶端發送資料請求,伺服器端接收請求並處理請求,然後把回應資料傳送給客戶端,客戶端讀取數據,最後關閉連接,一次互動結束。
這些介面的實作都是核心來完成。具體如何實現,可以看看linux的核心
4.1、socket()函數
int socket(int protofamily, int type, int protocol);
函數的三個參數分別為:
struct sockaddr_in {
sa_family_t sin_family; /* address family ; /* port in network byte order */
};
/* Internet address. */
struct in_addr {
byte order */
};
ipv6對應的是:
sa_family_t sin6_family; /* AF_INET6 */ cume sin6_family; /* AF_INET6 */ c /
uint32_t sin6_flowinfo; /* IPv6 flow information */
uint32_t sin6_scope_id; /* Scope ID (new in 2.4) */
};
struct in6_addr {
};
struct in6_addr {
};
struct in6_addr {
};
#define UNIX_PATH_MAX 108
struct sockaddr_un {
sa_family_t sun_family; path[UNIX_PATH_MAX]; /* pathname */
};
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
如果accept成功返回,則伺服器與客戶已經正確建立連接了,此時伺服器透過accept傳回的套接字來完成與客戶的通訊。
監聽套接字: 監聽套接字正如accept的參數sockfd,它是監聽套接字,在呼叫listen函數之後,是伺服器開始呼叫socket()函數產生的,稱為監聽socket描述字(監聽套接字)
連接套接字:一個套接字會從主動連接的套接字變身為一個監聽套接字;而accept函數返回的是已連接socket描述字(一個連接套接字),它代表著一個網路已經存在的點點連線。
一個伺服器通常通常只建立一個監聽socket描述字,它在該伺服器的生命週期內一直存在。核心為每個由伺服器程序接受的客戶連接創建了一個已連接socket描述字,當伺服器完成了對某個客戶的服務,相應的已連接socket描述字就被關閉。
自然要問的是:為什麼要有兩種套接字?原因很簡單,如果使用一個描述字的話,那麼它的功能太多,使得使用很不直觀,同時在內核確實產生了一個這樣的新的描述字。
連接套接字socketfd_new 並沒有佔用新的端口與客戶端通信,依然使用的是與監聽套接字socketfd一樣的端口號
4.5、read()、write()等函數
萬事具備只欠東風,至此伺服器與客戶已經建立好連線了。可以呼叫網路I/O進行讀寫操作了,也就是實現了網咯中不同進程之間的通訊!網路I/O操作有以下幾組:
read()/write()
recv()/send()
readv()/writev()
recvm()/senrecvm()
vsg()v )/sendto()
我推薦使用recvmsg()/sendmsg()函數,這兩個函數是最通用的I/O函數,實際上可以把上面的其它函數都替換成這兩個函數。它們的陳述如下:
#include
ssize_t read(int fd, void *buf, size_t count);
,
#include
#include
ssize_t send(int sockfd, const void *buf, size_t len, int flags); f, size_t len , int flags);
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
5 addr. t addrlen);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
read函數是負責從fd中讀取內容.當讀成功時,read返回實際所讀的位元組數,如果返回的值是0表示已經讀到文件的結束了,小於0表示出現了錯誤。如果錯誤為EINTR說明讀取是由中斷引起的,如果是ECONNREST表示網路連線出了問題。
write函數將buf中的nbytes位元組內容寫入檔案描述子fd.成功時傳回寫的位元組數。失敗時返回-1,並設定errno變數。 在網路程式中,當我們向套接字檔案描述符寫時有兩個可能。 1)write的回傳值大於0,表示寫了部分或全部的資料。 2)傳回的值小於0,此時出現了錯誤。我們要根據錯誤類型來處理。如果錯誤為EINTR表示寫的時候出現了中斷錯誤。如果為EPIPE表示網路連線出現了問題(對方已經關閉了連線)。
其它的我就不一一介紹這幾對I/O函數了,具體參見man文檔或者baidu、Google,下面的例子中將使用到send/recv。
4.6、close()函數
在伺服器與客戶端建立連線之後,會進行一些讀取與寫入操作,完成了讀取作業就要關閉對應的socket描述字,好比操作完開啟的檔案要呼叫fclose關閉打開的文件。
#include
close一個TCP socket的缺省行為時把該socket標記為以關閉,然後立即返回到調用進程。這個描述字不能再由呼叫程序使用,也就是說不能再當作read或write的第一個參數。
注意:close操作只是使對應socket描述字的參考計數-1,只有當引用計數為0的時候,才會觸發TCP客戶端向伺服器發送終止連線請求。
5. Socket中TCP的建立(三次握手)
TCPreeP的建立(三次握手)
TCPreeP. handshake),過程如下圖所示。
第一次握手:建立連線時,客戶端發送syn包(syn=j)到伺服器,並進入SYN_SEND狀態,等待伺服器確認;SYN:同步序號(Synchronize Sequence Numbers)。
第二次握手:伺服器收到syn包,必須確認客戶的SYN(ack=j+1),同時自己也發送一個SYN包(syn=k),即SYN+ACK包,此時伺服器進入SYN_RECV狀態;
第三次握手:客戶端收到伺服器的SYN+ACK包,向伺服器發送確認包ACK(ack=k+1),此包發送完畢,客戶端和伺服器進入ESTABLISHED狀態,完成三次握手。
一個完整的三次握手也就是: 請求---應答---再次確認。
對應的函數介面:
從圖中可以看出,當客戶端呼叫connect時,觸發了連接請求,向伺服器發送了SYN J包,這時connect進入阻塞狀態;伺服器監聽到連接請求,即收到SYN J包,呼叫accept函數接收請求向客戶端發送SYN K ,ACK J+1,這時accept進入阻塞狀態;客戶端收到伺服器的SYN K ,ACK J+1之後,這時connect返回,並對SYN K進行確認;伺服器收到ACK K+1時,accept返回,至此三次握手完畢,連線建立。
我們可以透過網路抓包的查看具體的流程:
例如我們伺服器開啟9502的連接埠。使用tcpdump來抓包:
tcpdump -iany tcp port 9502
然後我們使用telnet 127.0.
然後我們使用telnet 127.0.0.1 95020.7502050.050509120.0.0.
14:12:45.104687 IP localhost.39870 > localhost.9502: Flags [S], seq 2927179378, win 32792, options [mss 16396,sackOK,TS val 255474104 ecr0,104 ecr. 701 IP localhost.9502 > localhost.39870: Flags [S.], seq 1721825043, ack 2927179379, win 32768, options [mss 16396,sackOK,TS 40255, 405, ength 0 (2)
14 :12:45.104711 IP localhost.39870 > localhost.9502: Flags [.], ack 1, win 4099, options [nop,nop,TS val 255474104 ecr 255474104] 01.415407 IP localhost.39870 > localhost.9502: Flags [P.], seq 1:8, ack 1, win 4099, options [nop,nop,TS val 255478182 ecr 255474104], 18453:41474104], 4741041514741021147413213333330000142299:4100142141251342201321333300002133:41002142593:401 9502 > localhost.39870: Flags [.], ack 8, win 4096, options [nop,nop,TS val 255478182 ecr 255478182], length 0
14:13:01.415757 10785878:000 4189 19, win 4097, options [nop,nop,TS val 255478182 ecr 255478182], length 0
9870 > localhost.9502 表示通信的流向,39870是客戶端,9502是伺服器端
[.] 表示這是一個SYN+ACK確認包:
[.] 表示這是一個ACT確認包, (client)SYN->(server)SYN->(client)ACT 就是3次握手過程
[P] 表示這個是一個資料推送,可以是從伺服器端向客戶端推送,也可以從客戶端向伺服器端推
[F] 表示這是一個FIN包,是關閉連線操作,client/server都有可能發起
[R] 表示這是RST包,與F包作用相同,但RST表示連線關閉時,仍有資料未被處理。可以理解為是強制切割連接
win 4099 是指滑動視窗大小
length 18指封包的大小
我們看到 (1)(2)(3)三步是建立第一次握手:
14:12:45.104687 IP localhost.39870 > localhost.9502: Flags [S], seq 2927179378
客戶端IP localhost.39870 9502 發送syn包(syn=j)到伺服器》
syn包(syn=j) : syn的seq= 2927179378 (j=2927179378)
151501250151501515157500315150003151525303030338533833833個host .9502 > localhost.39870: Flags [S.], seq 1721825043, ack 2927179379,並確認:伺服器收到syn包,並必須確認客戶的SYN(ack=j+1),同時收到請求並確認:伺服器收到syn包,並必須確認客戶的SYN(ack=j+1),同時收到請求並確認發送一個SYN套件(syn=k),即SYN+ACK套件:此時伺服器主機自己的SYN:seq:y= syn seq 1721825043。
ACK為j+1 =(ack=j+1)=ack 2927179379
第三次握手:
14:12:45.104711 IP localhost.398709711 IP localhost. ,
客戶端收到伺服器的SYN+ACK包,向伺服器發送確認包ACK(ack=k+1)
连接出现连接不上的问题,一般是网路出现问题或者网卡超负荷或者是连接数已经满啦。
紫色背景的部分:
IP localhost.39870 > localhost.9502: Flags [P.], seq 1:8, ack 1, win 4099, options [nop,nop,TS val 255478182 ecr 255474104], length 7
客户端向服务器发送长度为7个字节的数据,
IP localhost.9502 > localhost.39870: Flags [.], ack 8, win 4096, options [nop,nop,TS val 255478182 ecr 255478182], length 0
服务器向客户确认已经收到数据
IP localhost.9502 > localhost.39870: Flags [P.], seq 1:19, ack 8, win 4096, options [nop,nop,TS val 255478182 ecr 255478182], length 18
然后服务器同时向客户端写入数据。
IP localhost.39870 > localhost.9502: Flags [.], ack 19, win 4097, options [nop,nop,TS val 255478182 ecr 255478182], length 0
客户端向服务器确认已经收到数据
这个就是tcp可靠的连接,每次通信都需要对方来确认。
6. Linux的SOCKET編程詳解
建立一个连接需要三次握手,而终止一个连接要经过四次握手,这是由TCP的半关闭(half-close)造成的,如图:
由于TCP连接是全双工的,因此每个方向都必须单独进行关闭。这个原则是当一方完成它的数据发送任务后就能发送一个FIN来终止这个方向的连接。收到一个 FIN只意味着这一方向上没有数据流动,一个TCP连接在收到一个FIN后仍能发送数据。首先进行关闭的一方将执行主动关闭,而另一方执行被动关闭。
(1)客户端A发送一个FIN,用来关闭客户A到服务器B的数据传送(报文段4)。
(2)服务器B收到这个FIN,它发回一个ACK,确认序号为收到的序号加1(报文段5)。和SYN一样,一个FIN将占用一个序号。
(3)服务器B关闭与客户端A的连接,发送一个FIN给客户端A(报文段6)。
(4)客户端A发回ACK报文确认,并将确认序号设置为收到序号加1(报文段7)。
Linux的SOCKET編程詳解如图:
过程如下:
某个应用进程首先调用close主动关闭连接,这时TCP发送一个FIN M;
另一端接收到FIN M之后,执行被动关闭,对这个FIN进行确认。它的接收也作为文件结束符传递给应用进程,因为FIN的接收意味着应用进程在相应的连接上再也接收不到额外数据;
一段时间之后,接收到文件结束符的应用进程调用close关闭它的socket。这导致它的TCP也发送一个FIN N;
接收到这个FIN的源发送端TCP对它进行确认。
这样每个方向上都有一个FIN和ACK。
1.为什么建立连接协议是三次握手,而关闭连接却是四次握手呢?
这是因为服务端的LISTEN状态下的SOCKET当收到SYN报文的建连请求后,它可以把ACK和SYN(ACK起应答作用,而SYN起同步作用)放在一个报文里来发送。但关闭连接时,当收到对方的FIN报文通知时,它仅仅表示对方没有数据发送给你了;但未必你所有的数据都全部发送给对方了,所以你可以未必会马上会关闭SOCKET,也即你可能还需要发送一些数据给对方之后,再发送FIN报文给对方来表示你同意现在可以关闭连接了,所以它这里的ACK报文和FIN报文多数情况下都是分开发送的。
2.为什么TIME_WAIT状态还需要等2MSL后才能返回到CLOSED状态?
这是因为虽然双方都同意关闭连接了,而且握手的4个报文也都协调和发送完毕,按理可以直接回到CLOSED状态(就好比从SYN_SEND状态到ESTABLISH状态那样);但是因为我们必须要假想网络是不可靠的,你无法保证你最后发送的ACK报文会一定被对方收到,因此对方处于LAST_ACK状态下的SOCKET可能会因为超时未收到ACK报文,而重发FIN报文,所以这个TIME_WAIT状态的作用就是用来重发可能丢失的ACK报文。
7. Socket编程实例
服务器端:一直监听本机的8000号端口,如果收到连接请求,将接收请求并接收客户端发来的消息,并向客户端返回消息。
/* File Name: server.c */ #include<stdio.h> #include<stdlib.h> #include<string.h> #include<errno.h> #include<sys/types.h> #include<sys/socket.h> #include<netinet/in.h> #define DEFAULT_PORT 8000 #define MAXLINE 4096 int main(int argc, char** argv) { int socket_fd, connect_fd; struct sockaddr_in servaddr; char buff[4096]; int n; //初始化Socket if( (socket_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1 ){ printf("create socket error: %s(errno: %d)\n",strerror(errno),errno); exit(0); } //初始化 memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY);//IP地址设置成INADDR_ANY,让系统自动获取本机的IP地址。 servaddr.sin_port = htons(DEFAULT_PORT);//设置的端口为DEFAULT_PORT //将本地地址绑定到所创建的套接字上 if( bind(socket_fd, (struct sockaddr*)&servaddr, sizeof(servaddr)) == -1){ printf("bind socket error: %s(errno: %d)\n",strerror(errno),errno); exit(0); } //开始监听是否有客户端连接 if( listen(socket_fd, 10) == -1){ printf("listen socket error: %s(errno: %d)\n",strerror(errno),errno); exit(0); } printf("======waiting for client's request======\n"); while(1){ //阻塞直到有客户端连接,不然多浪费CPU资源。 if( (connect_fd = accept(socket_fd, (struct sockaddr*)NULL, NULL)) == -1){ printf("accept socket error: %s(errno: %d)",strerror(errno),errno); continue; } //接受客户端传过来的数据 n = recv(connect_fd, buff, MAXLINE, 0); //向客户端发送回应数据 if(!fork()){ /*紫禁城*/ if(send(connect_fd, "Hello,you are connected!\n", 26,0) == -1) perror("send error"); close(connect_fd); exit(0); } buff[n] = '\0'; printf("recv msg from client: %s\n", buff); close(connect_fd); } close(socket_fd); }
客户端:
/* File Name: client.c */ #include<stdio.h> #include<stdlib.h> #include<string.h> #include<errno.h> #include<sys/types.h> #include<sys/socket.h> #include<netinet/in.h> #define MAXLINE 4096 int main(int argc, char** argv) { int sockfd, n,rec_len; char recvline[4096], sendline[4096]; char buf[MAXLINE]; struct sockaddr_in servaddr; if( argc != 2){ printf("usage: ./client <ipaddress>\n"); exit(0); } if( (sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0){ printf("create socket error: %s(errno: %d)\n", strerror(errno),errno); exit(0); } memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(8000); if( inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0){ printf("inet_pton error for %s\n",argv[1]); exit(0); } if( connect(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0){ printf("connect error: %s(errno: %d)\n",strerror(errno),errno); exit(0); } printf("send msg to server: \n"); fgets(sendline, 4096, stdin); if( send(sockfd, sendline, strlen(sendline), 0) < 0) { printf("send msg error: %s(errno: %d)\n", strerror(errno), errno); exit(0); } if((rec_len = recv(sockfd, buf, MAXLINE,0)) == -1) { perror("recv error"); exit(1); } buf[rec_len] = '\0'; printf("Received : %s ",buf); close(sockfd); exit(0); }
inet_pton 是Linux下IP地址转换函数,可以在将IP地址在“点分十进制”和“整数”之间转换 ,是inet_addr的扩展。
int inet_pton(int af, const char *src, void *dst);//转换字符串到网络地址:
第一个参数af是地址族,转换后存在dst中
af = AF_INET:src为指向字符型的地址,即ASCII的地址的首地址(ddd.ddd.ddd.ddd格式的),函数将该地址转换为in_addr的结构体,并复制在*dst中
af =AF_INET6:src为指向IPV6的地址,函数将该地址转换为in6_addr的结构体,并复制在*dst中
如果函数出错将返回一个负值,并将errno设置为EAFNOSUPPORT,如果参数af指定的地址族和src格式不对,函数将返回0。
测试:
编译server.c
gcc -o server server.c
启动进程:
./server
显示结果:
======waiting for client's request======
并等待客户端连接。
编译 client.c
gcc -o client server.c
客户端去连接server:
./client 127.0.0.1
等待输入消息
发送一条消息,输入:c++
此时服务器端看到:
客户端收到消息:
其实可以不用client,可以使用telnet来测试:
telnet 127.0.0.1 8000
注意:
在ubuntu 编译源代码的时候,头文件types.h可能找不到。
使用dpkg -L libc6-dev | grep types.h 查看。
如果没有,可以使用
apt-get install libc6-dev安装。
如果有了,但不在/usr/include/sys/目录下,手动把这个文件添加到这个目录下就可以了。