徒手在Linux上實現一個網絡協議棧《源碼筆記》

深度剖析網絡協議棧中的 socket 函數,可以說是把前面介紹的串聯起來,將網絡協議棧各層關聯起來。

徒手在Linux上實現一個網絡協議棧《源碼筆記》

一、應用層——recvfrom 函數

對於這個函數有必要分析一下,先看看這個dup例子。服務器端中調用recvfrom函數,並未指定發送端(客戶端)的地址,換句話說這個函數是一個被動函數,有點類似於tcp協議中服務器listen 之後阻塞,等待客戶端connect。這裡則是服務器端recvfrom後,等待客戶端sendto,服務器端recvfrom接收到客戶端的數據包,也順便知道了發送端的地址,於是將其填充到recvfrom的最後兩個參數中,這樣服務器端就獲得了客戶端的地址,然後服務器端就可sendto數據給客戶端。(TCP同理)

想想也是,服務器怎麼可能實現知道全球這麼多客戶的地址呢?但服務器採用的是大家廣為人知的地址,比如你訪問谷歌搜索,你知道谷歌的網址,但谷歌事先肯定不知道它眾多訪問者的地址,所以是客戶端先主動訪問,發送數據之後,谷歌才知道該客戶端的地址,然後返回訪問信息。

#include

ssize_t recvfrom(int sockfd, const void *buff, size_t nbytes, int flags,

const struct sockaddr *from, socklen_t *addrlen);

//若成功返回讀到的字節數,否則返回-1

/*參數解析。這裡sockfd是接收,from那邊是發送

前面三個參數分別表示:套接字描述符,指向寫出緩衝區的指針和寫字節數。

與sendto不同是後面的參數,recvfrom的最後兩個參數類似於accept的最後兩個參數,返回時其中套接字地址結構的內容告訴我們是誰發送了數據報

*/

二、BSD Socket 層——sock_recvfrom 函數

/*

*Receive a frame from the socket and optionally record the address of the

*sender. We verify the buffers are writable and if needed move the

*sender address from kernel to user space.

*/

//從指定的遠端地址接收數據,主要用於UDP協議

//從addr指定的源端接收len大小的數據,然後緩存到buff緩衝區

//該函數還要返回遠端地址信息,存放在addr指定的地址結構中

static int sock_recvfrom(int fd, void * buff, int len, unsigned flags,

struct sockaddr *addr, int *addr_len)

{

struct socket *sock;

struct file *file;

char address[MAX_SOCK_ADDR];

int err;

int alen;

//參數有效性檢查

if (fd < 0 || fd >= NR_OPEN || ((file = current->files->fd[fd]) == NULL))

return(-EBADF);

//通過文件描述符找到對應socket結構

if (!(sock = sockfd_lookup(fd, NULL)))

return(-ENOTSOCK);

if(len<0)

return -EINVAL;

if(len==0)

return 0;

//檢查緩衝區域是否可寫

err=verify_area(VERIFY_WRITE,buff,len);

if(err)

return err;

//調用下層函數inet_recvfrom

len=sock->ops->recvfrom(sock, buff, len, (file->f_flags & O_NONBLOCK),

flags, (struct sockaddr *)address, &alen);

if(len<0)

return len;

//對比可知,這裡是sock_recvfrom相比sock_sendto多出來的一部分

//它的作用便是將發送端(客戶端)的地址信息填充到addr中,就是獲取客戶端的地址信息

if(addr!=NULL && (err=move_addr_to_user(address,alen, addr, addr_len))<0)

return err;

return len;

}

三、INET Socket 層——inet_recvfrom 函數

/*

*The assorted BSD I/O operations

*/

//其功能與inet_sendto函數類似

static int inet_recvfrom(struct socket *sock, void *ubuf, int size, int noblock,

unsigned flags, struct sockaddr *sin, int *addr_len )

{

//獲取對應sock結構

struct sock *sk = (struct sock *) sock->data;

if (sk->prot->recvfrom == NULL)

return(-EOPNOTSUPP);

if(sk->err)

return inet_error(sk);

/* We may need to bind the socket. */

//檢查是否綁定了端口,沒有的話就自動綁定一個,就服務器端而言,肯定是有的

if(inet_autobind(sk)!=0)

return(-EAGAIN);

//調用下層udp_recvfrom函數

return(sk->prot->recvfrom(sk, (unsigned char *) ubuf, size, noblock, flags,

(struct sockaddr_in*)sin, addr_len));


四、傳輸層——udp_recvfrom 函數

/*

* This should be easy, if there is something there we\\

* return it, otherwise we block.

*/

//接收數據包,並返回對端地址(如果需要的話)

int udp_recvfrom(struct sock *sk, unsigned char *to, int len,

int noblock, unsigned flags, struct sockaddr_in *sin,

int *addr_len)

{

int copied = 0;

int truesize;

struct sk_buff *skb;

int er;

/*

*Check any passed addresses

*/

if (addr_len)

*addr_len=sizeof(*sin);

/*

*From here the generic datagram does a lot of the work. Come

*the finished NET3, it will do _ALL_ the work!

*/

//從接收隊列中獲取數據包

skb=skb_recv_datagram(sk,flags,noblock,&er);

if(skb==NULL)

return er;

//數據包數據部分(數據報)長度

truesize = skb->len;

//讀取長度檢查設置,udp是面向報文的,其接收到的每個數據包都是獨立的

//如果用戶要求讀取的小於可讀取的,那麼剩下的將被丟棄(本版本協議棧就是這麼幹的)

copied = min(len, truesize);

/*

*FIXME : should use udp header size info value

*/

//拷貝skb數據包中的數據負載到to緩衝區中

//這裡就是數據轉移的地方,將數據從數據包中轉移出來到緩存區

skb_copy_datagram(skb,sizeof(struct udphdr),to,copied);

sk->stamp=skb->stamp;//記錄時間

/* Copy the address. */

//如果要求返回遠端地址的話,這裡就是拷貝遠端地址信息了,含端口號和ip地址

if (sin)

{

sin->sin_family = AF_INET;//地址族

sin->sin_port = skb->h.uh->source;//端口號

sin->sin_addr.s_addr = skb->daddr;//ip地址,這裡是目的ip地址,有點困惑?

}

//釋放該數據包

skb_free_datagram(skb);

release_sock(sk);

return(truesize);//返回讀取(接收)到的數據的大小

}

上面在數據處理方面,調用了三個數據報文處理函數(net\\inet\\Datagram.c):skb_recv_datagram()、skb_copy_datagram()、skb_free_datagram()

skb_recv_datagram()

/*

*Get a datagram skbuff, understands the peeking, nonblocking wakeups and possible

*races. This replaces identical code in packet,raw and udp, as well as the yet to

*be released IPX support. It also finally fixes the long standing peek and read

*race for datagram sockets. If you alter this routine remember it must be

*re-entrant.

*/

//從接收隊列中獲取數據包

//需要注意的是,這些函數(非udp.c文件下)或沒有明確指明只與udp協議相關的函數則都是通用的

//在tcp和udp協議下都可被調用

struct sk_buff *skb_recv_datagram(struct sock *sk, unsigned flags, int noblock, int *err)

{

struct sk_buff *skb;

unsigned long intflags;

/* Socket is inuse - so the timer doesn't attack it */

save_flags(intflags);

restart:

sk->inuse = 1;//加鎖

//檢查套接字接收隊列中是否有數據包

//如果沒有,則睡眠等待,在睡眠等待之前必須檢查等待的必要性

while(skb_peek(&sk->receive_queue) == NULL)/* No data */

{

/* If we are shutdown then no more data is going to appear. We are done */

//檢查套接字是否已經被關閉接收通道,已經關閉通道了就沒必要盲目等待了

if (sk->shutdown & RCV_SHUTDOWN)

{

release_sock(sk);//對於udp無用,因為udp沒有采用back_log暫存隊列

*err=0;

return NULL;

}

//發生錯誤,則需要首先處理錯誤,返回

if(sk->err)

{

release_sock(sk);

*err=-sk->err;

sk->err=0;

return NULL;

}

/* Sequenced packets can come disconnected. If so we report the problem */

//狀態檢查,如果不符合則置錯誤標誌並返回

if(sk->type==SOCK_SEQPACKET && sk->state!=TCP_ESTABLISHED)

{

release_sock(sk);

*err=-ENOTCONN;

return NULL;

}

/* User doesn't want to wait */

//不阻塞,即調用者要求不進行睡眠等待,則直接返回

if (noblock)

{

release_sock(sk);

*err=-EAGAIN;

return NULL;

}

//系列篇前面介紹過該函數的一個主要功能是重新接收back_log緩存隊列中的數據包

//由於udp協議不會使用back_log隊列(用於tcp超時重發),所以該函數不會對套接字接收隊列造成影響

release_sock(sk);

/* Interrupts off so that no packet arrives before we begin sleeping.

Otherwise we might miss our wake up */

cli();

//經過前面的一系列檢查,這裡再次判斷是否隊列中沒有數據包

//因為很有可能在上面檢查過程中,有數據包到達

if (skb_peek(&sk->receive_queue) == NULL)

{

interruptible_sleep_on(sk->sleep);//進入睡眠等待

/* Signals may need a restart of the syscall */

if (current->signal & ~current->blocked)

{

restore_flags(intflags);;

*err=-ERESTARTSYS;

return(NULL);

}

if(sk->err != 0)/* Error while waiting for packet

eg an icmp sent earlier by the

peer has finally turned up now */

{

*err = -sk->err;

sk->err=0;

restore_flags(intflags);

return NULL;

}

}

sk->inuse = 1;//該套接字目前正在被本進程使用,不能被其餘場所使用

restore_flags(intflags);//恢復現場

}//end while

/* Again only user level code calls this function, so nothing interrupt level

will suddenly eat the receive_queue */

//如果接收隊列中存在數據包

//處理正常讀取的情況

if (!(flags & MSG_PEEK))

{

skb=skb_dequeue(&sk->receive_queue);//從隊列中獲取數據包

if(skb!=NULL)

skb->users++;//使用該數據包的模塊數+1

else

goto restart;/* Avoid race if someone beats us to the data */

}

//如果設置了MSG_PEEK標誌,允許查看已可讀取的數據

//處理預先讀取的情況

else

{

cli();

skb=skb_peek(&sk->receive_queue);

if(skb!=NULL)

skb->users++;

restore_flags(intflags);

if(skb==NULL)/* shouldn't happen but .. */

*err=-EAGAIN;

}

return skb;//返回該數據包

}

skb_copy_datagram()

//將內核緩衝區中數據複製到用戶緩衝區

//拷貝size大小skb數據包中的數據負載(由offset偏移定位)到to緩衝區中

void skb_copy_datagram(struct sk_buff *skb, int offset, char *to, int size)

{

/* We will know all about the fraglist options to allow >4K receives

but not this release */

//函數原型:memcpy_tofs(to,from,n) :功能一目瞭然

memcpy_tofs(to,skb->h.raw+offset,size);

}

skb_free_datagram()

//釋放一個數據包

//先判斷該數據包是否還有其餘模塊使用,再判斷該數據包是否還處於系統的某個隊列中,

//換句話說,這兩個判斷的目的就是看該數據包是否還有用,沒有用了就釋放

void skb_free_datagram(struct sk_buff *skb)

{

unsigned long flags;

save_flags(flags);//保存現場

cli();

skb->users--;//使用該數據包的模塊數-1

if(skb->users>0)//如果還有模塊使用該數據包,則直接返回

{

restore_flags(flags);

return;

}

/* See if it needs destroying */

//如果沒有其餘模塊使用該數據包,表示這是一個遊離的數據包

//下面檢查數據包是否仍處於系統某個隊列中,如果還處於某個隊列中則不可進行釋放

if(!skb->next && !skb->prev)/* Been dequeued by someone - ie it's read */

kfree_skb(skb,FREE_READ);//否則釋放該數據包所佔用的內存空間

restore_flags(flags);//恢復現場

}

對比數據包的發送與接收,發送過程就是把數據從緩衝區拷貝到數據包的數據部分,由於需要經過協議棧,所以對於數據部分區域還需要進行數據封裝,添加各層的協議頭。對於數據包的接收,由於本來已經處於傳輸層了,不需要進行數據包的解封裝,直接獲取套接字接收隊列中的數據包(如果有),然後再將數據包中的數據部分拷貝到緩衝區。

總結:今天就分享到這吧,需要的相關資料+學習+問題的小夥伴可以私信;資料;免費領取,

資料內容:包括C/C++,Linux,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒體,CDN,P2P,K8S,Docker,Golang,TCP/IP,協程,DPDK等等。。。


徒手在Linux上實現一個網絡協議棧《源碼筆記》


徒手在Linux上實現一個網絡協議棧《源碼筆記》


分享到:


相關文章: