1. 程式人生 > >UDP傳送資料包流程

UDP傳送資料包流程

UDP傳送資料包的函式是udp_sendmsg,完成從使用者地址空間接受資料包然後賦值到核心空間。udp_sendmsg函式主輸入引數有四個:

(1)、kiocb:為了提高對使用者地址空間操作效率的資料結構體。

(2)、sk:開啟的套接字資料結構,包含了套接字的所有設定資訊和選項。

(3)、msg:存放管理使用者地址空間的資料結構。

(4)、len:從使用者空間接受的資料包長度。

int udp_sendmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg,
		size_t len)

struct msghdr結構體:

struct msghdr {
	void	*	msg_name;	/* Socket name		目的地址選項	*/
	int		msg_namelen;	/* Length of name		目的地址長度*/
	struct iovec *	msg_iov;	/* Data blocks		訊息陣列	*/
	__kernel_size_t	msg_iovlen;	/* Number of blocks		*/
	void 	*	msg_control;	/* Per protocol magic (eg BSD file descriptor passing) 控制資訊*/
	__kernel_size_t	msg_controllen;	/* Length of cmsg list */
	unsigned	msg_flags;	//接受資料的標誌
};

1、正確性檢查

首先是做資料包的正確檢查,如果傳送錯誤比如資料指標越記憶體了可能作業系統崩潰,所以必須做資料的正確性檢查,首先檢查資料包長度是否小於0xFFFF,因為udp資料包的長度表示位最大16位,然後檢查套接字標誌是否設定位非法的MSG_OOB(允許帶外發送資料)

...

	//檢查資料包長度
	if (len > 0xFFFF)
		return -EMSGSIZE;

	/*
	 *	Check the flags.
	 */
	//套接字非法標誌
	if (msg->msg_flags & MSG_OOB) /* Mirror BSD error message compatibility */
		return -EOPNOTSUPP;

...

2、處理早期懸掛的資料

先檢查當前套接字是否有掛起等待發送的資料,如果有就跳轉到do_append_data標籤處,先處理掛起等待發送的資料包,

...

//是否有掛起等待發送的資料包
	if (up->pending) {
		/*
		 * There are pending frames.
		 * The socket lock must be held while it's corked.
		 */
		lock_sock(sk);
		if (likely(up->pending)) {
			//掛起的資料包是否是AF_INET協議族
			if (unlikely(up->pending != AF_INET)) {
				release_sock(sk);
				return -EINVAL;
			}
			//先處理掛起的資料包複製到IP層
			goto do_append_data;
		}
		release_sock(sk);
	}

...

3、處理新的資料

如果沒有掛起等待發送的資料包,udp_sendmsg就處理使用者空間傳來的資料。新資料處理主要有三個方面:目的IP地址檢查、判斷是否已經建立連線、控制資訊處理。

(1)、目的IP檢查

如果目的Ip地址msg->msg_name不為空,就要檢查目的ip地址,目的ip地址由套接字名給出,而套接字名儲存在msg->msg_name資料域,首先檢查目的Ip地址的長度和協議族是否是AF_INET,如果不是就返回錯誤,檢查通過後將目的ip和目的埠賦值給區域性變數daddr、dport。

...

 //檢查目的IP
	if (msg->msg_name) {
		struct sockaddr_in * usin = (struct sockaddr_in *)msg->msg_name;
		//目的地址長度檢查
		if (msg->msg_namelen < sizeof(*usin))
			return -EINVAL;
		//目的地址協議族檢查
		if (usin->sin_family != AF_INET) {
			if (usin->sin_family != AF_UNSPEC)
				return -EAFNOSUPPORT;
		}
		//目的ip目的埠賦值
		daddr = usin->sin_addr.s_addr;
		dport = usin->sin_port;
		if (dport == 0)
			return -EINVAL;
	} else {

...

(2)、已經建立連線

msg->msg_name中的目的地址為空時就要檢查是否已經建立連線狀況,判斷sk->sk_state是否等於TCP_ESTABLISHED,如果不是就返回錯誤無效的目的地址,如果sk->sk_state等於TCP_ESTABLISHED說明目的路由儲存在路由高速緩暫存器中,這時即使應用層傳進來來目的ip為空也可以傳送資料包,從Inet中取出目的ip和目的埠賦值給區域性變數daddr、dport。

    ...

    //目的IP為空檢查連線狀態是否為TCP_ESTABLISHED
		if (sk->sk_state != TCP_ESTABLISHED)
			return -EDESTADDRREQ;
		//將連線狀態的路由資訊中目的ip目的埠賦值
		daddr = inet->inet_daddr;
		dport = inet->inet_dport;
		/* Open fast path for connected socket.
		   Route will not be used, if at least one option is set.
		 */
		connected = 1;

...

(3)、控制資訊處理

處理和目的IP後就要處理udp的控制資訊,udp控制資訊儲存在msg->msg_control資料域中,如果msg->msg_controllen不為零說明有控制資訊,控制資訊通過函式ip_cmsg_send解析儲存在區域性變數struct ipcm_cookie  ipc中。

struct ipcm_cookie{
    __be32              addr;        //輸出網路裝置地址
    int                 oif          //輸出網路裝置索引
    struct ip_option    *opt;        //ip選項
}

ip_cmsg_send處理udp的控制資訊主要有兩種:

a、IP_RETOPTS:從ip協議頭中獲取ip選項儲存到ipc->opt中

b、IP_PKTINFO:主要講網路裝置的索引和ip地址返回為ipc->addr、ipc->oif

int ip_cmsg_send(struct net *net, struct msghdr *msg, struct ipcm_cookie *ipc)
{
	int err;
	struct cmsghdr *cmsg;

	for (cmsg = CMSG_FIRSTHDR(msg); cmsg; cmsg = CMSG_NXTHDR(msg, cmsg)) {
		if (!CMSG_OK(msg, cmsg))
			return -EINVAL;
		if (cmsg->cmsg_level != SOL_IP)
			continue;
		switch (cmsg->cmsg_type) {
           //ip選項獲取
		case IP_RETOPTS:
			err = cmsg->cmsg_len - CMSG_ALIGN(sizeof(struct cmsghdr));
            //從ip協議頭中獲取Ip選項
			err = ip_options_get(net, &ipc->opt, CMSG_DATA(cmsg),
					     err < 40 ? err : 40);
			if (err)
				return err;
			break;
            //獲取輸出裝置網路索引和介面ip
		case IP_PKTINFO:
		{
			struct in_pktinfo *info;
			if (cmsg->cmsg_len != CMSG_LEN(sizeof(struct in_pktinfo)))
				return -EINVAL;
			info = (struct in_pktinfo *)CMSG_DATA(cmsg);
            //獲取輸出網路裝置索引
			ipc->oif = info->ipi_ifindex;
            //獲取介面地址
			ipc->addr = info->ipi_spec_dst.s_addr;
			break;
		}
		default:
			return -EINVAL;
		}
	}
	return 0;
}

如果套接字中沒有設定ip選項就從inet中獲取ip選項資料域

...

   //設定了套接字控制資訊
	if (msg->msg_controllen) {
		//獲取套接字控制資訊儲存在ipc結構體中
		err = ip_cmsg_send(sock_net(sk), msg, &ipc);
		if (err)
			return err;
		if (ipc.opt)
			free = 1;
		connected = 0;
	}
	//如果沒有設定控制資訊
	//就從inet中獲取ip選項
	if (!ipc.opt)
		ipc.opt = inet->opt;
    
    //介面地址複製區域性變數saddr
	saddr = ipc.addr;
    //目的地址複製ipc.addr
	ipc.addr = faddr = daddr;

...

4、路由判斷

向ip層傳送資料,首先要判斷路由,路由的情況有三種

4.1、不需要設定路由

不需要設定路由的主要有四種情況

a、資料包是本地區域網傳送標誌是sk->localroute。

b、輸入資訊選項msg->msg_flags設定了不需要路由標誌。

c、ip選項設定了嚴格路由選項。

d、目的地址是組地址也不要設定路由

...

tos = RT_TOS(inet->tos);
	//如果資料是本地區域網傳送標誌SOCK_LOCALROUTE
	//或者不需要路由msg_flags標誌MSG_DONTROUTE
	//或者配置了嚴格路由,就不需要定址路由tos設定為RTO+ONLINK
	if (sock_flag(sk, SOCK_LOCALROUTE) ||
	    (msg->msg_flags & MSG_DONTROUTE) ||
	    (ipc.opt && ipc.opt->is_strictroute)) {
		tos |= RTO_ONLINK;
		connected = 0;
	}

	//目的地址是組地址也不需要定址路由
	if (ipv4_is_multicast(daddr)) {
		if (!ipc.oif)
			ipc.oif = inet->mc_index;
		if (!saddr)
			saddr = inet->mc_addr;
		connected = 0;
	}
...

4.2、路由已知

路由已知也就是connected標誌位1,就從路由高速緩衝區暫存器中區路由儲存到區域性變數rt中

...

//路由已知,檢查目的路由並賦值給路由高速緩衝區的區域性變數rt
	if (connected)
		rt = (struct rtable *)sk_dst_check(sk, 0);
...

4.3、目的路由無效

檢查到目的路由rt是NULL,就要呼叫ip_route_output_flow函式從路由表中搜索路由,要搜尋路由首先要建立struct flowi結構體,主要更加源ip、目的ip、源埠、目的埠、輸出網路裝置獲取路由。如果獲取的路由是一個廣播路由,但套接字沒有設定SO_BROADCAST選項就返回錯誤。

...

//目的路由無效
	if (rt == NULL) {
		struct flowi fl = { .oif = ipc.oif,
				    .mark = sk->sk_mark,
				    .nl_u = { .ip4_u =
					      { .daddr = faddr,
						.saddr = saddr,
						.tos = tos } },
				    .proto = sk->sk_protocol,
				    .flags = inet_sk_flowi_flags(sk),
				    .uli_u = { .ports =
					       { .sport = inet->inet_sport,
						 .dport = dport } } };
		struct net *net = sock_net(sk);

		security_sk_classify_flow(sk, &fl);
		//搜尋路由表建立目的路由
		err = ip_route_output_flow(net, &rt, &fl, sk, 1);
		if (err) {
			if (err == -ENETUNREACH)
				IP_INC_STATS_BH(net, IPSTATS_MIB_OUTNOROUTES);
			goto out;
		}

		err = -EACCES;
		//目的路由是廣播路由,但套接字沒有設定SO_BROADCAST就返回錯誤
		if ((rt->rt_flags & RTCF_BROADCAST) &&
		    !sock_flag(sk, SOCK_BROADCAST))
			goto out;
		if (connected)
			//建立路由連線,儲存路由資訊到區域性變數rt
			sk_dst_set(sk, dst_clone(&rt->u.dst));
	}

...

4.4、路由已知

到目的地址的有效路由已經建立,判斷套接字選項是否設定了MSG_CONFIRM(套接字返回有效路由資訊),如果設定了就要跳轉到返回路由資訊處理標籤:do_confirm


...

    //套接字設定了MSG_CONFIRM標誌
	//調轉到返回路由資訊處理標籤
	if (msg->msg_flags&MSG_CONFIRM)
		goto do_confirm;

...

5、向IP層傳送資料

向ip層傳送資料主要分三個步驟

(1)、加鎖,如果套接字已經阻塞就是否套接字並返回錯誤資訊

...

lock_sock(sk); 
	
	if (unlikely(up->pending)) {
		/* The socket is already corked while preparing it. */
		/* ... which is an evident application bug. --ANK */
		//套接字已經阻塞,則釋放套接字
		release_sock(sk);

		LIMIT_NETDEBUG(KERN_DEBUG "udp cork app bug 2\n");
		err = -EINVAL;
		goto out;
	}
...

(2)、為套接字新增源ip、目的Ip、源埠、目的埠

...

//鎖定成功,新增源IP、目的IP、等資訊準備傳送資料
	inet->cork.fl.fl4_dst = daddr;
	inet->cork.fl.fl_ip_dport = dport;
	inet->cork.fl.fl4_src = saddr;
	inet->cork.fl.fl_ip_sport = inet->inet_sport;
	up->pending = AF_INET;

...

(3)、呼叫Ip_append_data把資料包複製到IP層緩衝區,getfrag複製把資料包從應用層複製到IP層緩衝區,如果corkreq沒有設定MSG_MORE標誌,那麼立即呼叫udp_push_pending_frames把剛快取的資料包傳送出去。

...

do_append_data:
	up->len += ulen;
	getfrag  =  is_udplite ?  udplite_getfrag : ip_generic_getfrag;
	//緩衝資料包
	err = ip_append_data(sk, getfrag, msg->msg_iov, ulen,
			sizeof(struct udphdr), &ipc, &rt,
			corkreq ? msg->msg_flags|MSG_MORE : msg->msg_flags);
	//快取資料包失敗刪除把skb從sk_write_queue佇列中釋放
	if (err)
		udp_flush_pending_frames(sk);
	//corkreq標誌沒有設定MSG_MORE立即呼叫udp_push_pending_frames傳送
	//傳送剛快取的資料包
	else if (!corkreq)
		err = udp_push_pending_frames(sk);
	else if (unlikely(skb_queue_empty(&sk->sk_write_queue)))
		up->pending = 0;
	//釋放sk
	release_sock(sk);

...

6、ip_generic_getfrag函式

ip_generic_getfrag函式複製把資料從使用者空間複製到核心空間,如果複製時不需要做校驗和就呼叫memcpy_formiovecend,校驗和留給硬體去做,如果需要做校驗和就呼叫csum_partial_copy_fromviovecend計算部分校驗和在複製到IP層。udp校驗和分三個階段:資料校驗和、udp協議頭校驗和、ip協議頭校驗和。最終將資料從iov指標指向的使用者地址空間複製到核心地址空間的區域性緩衝區sk_buff中。

ip_generic_getfrag函式:

int
ip_generic_getfrag(void *from, char *to, int offset, int len, int odd, struct sk_buff *skb)
{
	struct iovec *iov = from;

	if (skb->ip_summed == CHECKSUM_PARTIAL) {
		//複製時不需要計算校驗和
		if (memcpy_fromiovecend(to, iov, offset, len) < 0)
			return -EFAULT;
	} else {
		__wsum csum = 0;
		//計算校驗和並複製資料
		if (csum_partial_copy_fromiovecend(to, iov, offset, len, &csum) < 0)
			return -EFAULT;
		skb->csum = csum_block_add(skb->csum, csum, odd);
	}
	return 0;
}

struct iov結構體

struct iovec
{
	void __user *iov_base;	/* 應用層資料包存放在緩衝區的地址*/
	__kernel_size_t iov_len; /* 緩衝區能接受的最大資料或者能寫入的實際資料長度 */
};

memcpy_fromiovecend函式:

int memcpy_fromiovecend(unsigned char *kdata, const struct iovec *iov,
			int offset, int len)
{
	/* Skip over the finished iovecs */
	//跳過已經複製完成的資料
	while (offset >= iov->iov_len) {
		offset -= iov->iov_len;
		iov++;
	}

	while (len > 0) {
		//取資料的起始地址
		u8 __user *base = iov->iov_base + offset;
		//iov->iov_len是struct iov結構中儲存資料的最大長度、offset需要複製資料包的偏移量
		//取len和iov->iov_len-offset的最小值
		int copy = min_t(unsigned int, len, iov->iov_len - offset);

		offset = 0;
		//拷貝資料到核心空間
		if (copy_from_user(kdata, base, copy))
			return -EFAULT;
		len -= copy;
		kdata += copy;
		iov++;
	}

	return 0;
}

udp_sendmsg函式程式碼:

int udp_sendmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg,
		size_t len)
{
	struct inet_sock *inet = inet_sk(sk);
	struct udp_sock *up = udp_sk(sk);
	int ulen = len;
	//控制資訊結構體
	struct ipcm_cookie ipc;
	//路由高速緩衝去入口連結串列
	struct rtable *rt = NULL;
	int free = 0;
	//路由連線標誌
	int connected = 0;
	__be32 daddr, faddr, saddr;
	__be16 dport;
	u8  tos;
	int err, is_udplite = IS_UDPLITE(sk);
	int corkreq = up->corkflag || msg->msg_flags&MSG_MORE;
	int (*getfrag)(void *, char *, int, int, int, struct sk_buff *);

	//檢查資料包長度
	if (len > 0xFFFF)
		return -EMSGSIZE;

	/*
	 *	Check the flags.
	 */
	//套接字非法標誌
	if (msg->msg_flags & MSG_OOB) /* Mirror BSD error message compatibility */
		return -EOPNOTSUPP;

	ipc.opt = NULL;
	ipc.shtx.flags = 0;

	//是否有掛起等待發送的資料包
	if (up->pending) {
		/*
		 * There are pending frames.
		 * The socket lock must be held while it's corked.
		 */
		lock_sock(sk);
		if (likely(up->pending)) {
			//掛起的資料包是否是AF_INET協議族
			if (unlikely(up->pending != AF_INET)) {
				release_sock(sk);
				return -EINVAL;
			}
			//先處理掛起的資料包複製到IP層
			goto do_append_data;
		}
		release_sock(sk);
	}
	ulen += sizeof(struct udphdr);

	/*
	 *	Get and verify the address.
	 */
	 //檢查目的IP
	if (msg->msg_name) {
		struct sockaddr_in * usin = (struct sockaddr_in *)msg->msg_name;
		//目的地址長度檢查
		if (msg->msg_namelen < sizeof(*usin))
			return -EINVAL;
		//目的地址協議族檢查
		if (usin->sin_family != AF_INET) {
			if (usin->sin_family != AF_UNSPEC)
				return -EAFNOSUPPORT;
		}
		//目的ip目的埠賦值
		daddr = usin->sin_addr.s_addr;
		dport = usin->sin_port;
		if (dport == 0)
			return -EINVAL;
	} else {
		//目的IP為空檢查連線狀態是否為TCP_ESTABLISHED
		if (sk->sk_state != TCP_ESTABLISHED)
			return -EDESTADDRREQ;
		//將連線狀態的路由資訊中目的ip目的埠賦值
		daddr = inet->inet_daddr;
		dport = inet->inet_dport;
		/* Open fast path for connected socket.
		   Route will not be used, if at least one option is set.
		 */
		connected = 1;
	}
	//輸出網路裝置地址
	ipc.addr = inet->inet_saddr;
	//輸出網路裝置索引號
	ipc.oif = sk->sk_bound_dev_if;
	err = sock_tx_timestamp(msg, sk, &ipc.shtx);
	if (err)
		return err;
	//設定了套接字控制資訊
	if (msg->msg_controllen) {
		//獲取套接字控制資訊儲存在ipc結構體中
		err = ip_cmsg_send(sock_net(sk), msg, &ipc);
		if (err)
			return err;
		if (ipc.opt)
			free = 1;
		connected = 0;
	}
	//如果沒有設定控制資訊
	//就從inet中獲取ip選項
	if (!ipc.opt)
		ipc.opt = inet->opt;
    
    //介面地址複製區域性變數saddr
	saddr = ipc.addr;
    //目的地址複製ipc.addr
	ipc.addr = faddr = daddr;

	//如果設定了源路由
	//那麼下一站點的目的地址從源路由的IP地址列表中獲取
	if (ipc.opt && ipc.opt->srr) {
		if (!daddr)
			return -EINVAL;
		faddr = ipc.opt->faddr;
		connected = 0;
	}
	tos = RT_TOS(inet->tos);
	//如果資料是本地區域網傳送標誌SOCK_LOCALROUTE
	//或者不需要路由msg_flags標誌MSG_DONTROUTE
	//或者配置了嚴格路由,就不需要定址路由tos設定為RTO+ONLINK
	if (sock_flag(sk, SOCK_LOCALROUTE) ||
	    (msg->msg_flags & MSG_DONTROUTE) ||
	    (ipc.opt && ipc.opt->is_strictroute)) {
		tos |= RTO_ONLINK;
		connected = 0;
	}

	//目的地址是組地址也不需要定址路由
	if (ipv4_is_multicast(daddr)) {
		if (!ipc.oif)
			ipc.oif = inet->mc_index;
		if (!saddr)
			saddr = inet->mc_addr;
		connected = 0;
	}

	//路由已知,檢查目的路由並賦值給路由高速緩衝區的區域性變數rt
	if (connected)
		rt = (struct rtable *)sk_dst_check(sk, 0);

	//目的路由無效
	if (rt == NULL) {
		struct flowi fl = { .oif = ipc.oif,
				    .mark = sk->sk_mark,
				    .nl_u = { .ip4_u =
					      { .daddr = faddr,
						.saddr = saddr,
						.tos = tos } },
				    .proto = sk->sk_protocol,
				    .flags = inet_sk_flowi_flags(sk),
				    .uli_u = { .ports =
					       { .sport = inet->inet_sport,
						 .dport = dport } } };
		struct net *net = sock_net(sk);

		security_sk_classify_flow(sk, &fl);
		//搜尋路由表建立目的路由
		err = ip_route_output_flow(net, &rt, &fl, sk, 1);
		if (err) {
			if (err == -ENETUNREACH)
				IP_INC_STATS_BH(net, IPSTATS_MIB_OUTNOROUTES);
			goto out;
		}

		err = -EACCES;
		//目的路由是廣播路由,但套接字沒有設定SO_BROADCAST就返回錯誤
		if ((rt->rt_flags & RTCF_BROADCAST) &&
		    !sock_flag(sk, SOCK_BROADCAST))
			goto out;
		if (connected)
			//建立路由連線,儲存路由資訊到區域性變數rt
			sk_dst_set(sk, dst_clone(&rt->u.dst));
	}
	//套接字設定了MSG_CONFIRM標誌
	//調轉到返回路由資訊處理標籤
	if (msg->msg_flags&MSG_CONFIRM)
		goto do_confirm;
back_from_confirm:

	saddr = rt->rt_src;
	if (!ipc.addr)
		daddr = ipc.addr = rt->rt_dst;

	lock_sock(sk); 
	
	if (unlikely(up->pending)) {
		/* The socket is already corked while preparing it. */
		/* ... which is an evident application bug. --ANK */
		//套接字已經阻塞,則釋放套接字
		release_sock(sk);

		LIMIT_NETDEBUG(KERN_DEBUG "udp cork app bug 2\n");
		err = -EINVAL;
		goto out;
	}
	/*
	 *	Now cork the socket to pend data.
	 */
	 //鎖定成功,新增源IP、目的IP、等資訊準備傳送資料
	inet->cork.fl.fl4_dst = daddr;
	inet->cork.fl.fl_ip_dport = dport;
	inet->cork.fl.fl4_src = saddr;
	inet->cork.fl.fl_ip_sport = inet->inet_sport;
	up->pending = AF_INET;

do_append_data:
	up->len += ulen;
	getfrag  =  is_udplite ?  udplite_getfrag : ip_generic_getfrag;
	//緩衝資料包
	err = ip_append_data(sk, getfrag, msg->msg_iov, ulen,
			sizeof(struct udphdr), &ipc, &rt,
			corkreq ? msg->msg_flags|MSG_MORE : msg->msg_flags);
	//快取資料包失敗刪除把skb從sk_write_queue佇列中釋放
	if (err)
		udp_flush_pending_frames(sk);
	//corkreq標誌沒有設定MSG_MORE立即呼叫udp_push_pending_frames傳送
	//傳送剛快取的資料包
	else if (!corkreq)
		err = udp_push_pending_frames(sk);
	else if (unlikely(skb_queue_empty(&sk->sk_write_queue)))
		up->pending = 0;
	//釋放sk
	release_sock(sk);

out:
	ip_rt_put(rt);
	if (free)
		kfree(ipc.opt);
	if (!err)
		return len;
	/*
	 * ENOBUFS = no kernel mem, SOCK_NOSPACE = no sndbuf space.  Reporting
	 * ENOBUFS might not be good (it's not tunable per se), but otherwise
	 * we don't have a good statistic (IpOutDiscards but it can be too many
	 * things).  We could add another new stat but at least for now that
	 * seems like overkill.
	 */
	if (err == -ENOBUFS || test_bit(SOCK_NOSPACE, &sk->sk_socket->flags)) {
		UDP_INC_STATS_USER(sock_net(sk),
				UDP_MIB_SNDBUFERRORS, is_udplite);
	}
	return err;

do_confirm:
	dst_confirm(&rt->u.dst);
	if (!(msg->msg_flags&MSG_PROBE) || len)
		goto back_from_confirm;
	err = 0;
	goto out;
}