Linux网络协议栈从应用层到内核层③

文章目录

  • 1、write源码剖析
  • 2、vfs层进行数据传输
  • 3、socket层进行数据传输
  • 4、tcp层进行数据传输
  • 5、ip层进行数据传输
  • 6、网络设备层进行数据传输
  • 7、网卡驱动层进行数据传输
  • 8、数据传输的整个流程

1、write源码剖析

系统调用原型

ssize_t write(int fildes, const void *buf, size_t nbyte);

fildes:文件描述符
buf:用户缓冲区,用于存放要写入的数据
nbyte:用户缓冲区的大小
返回值表示成功写入了多少字节的数据,因为write并不保证一定将数据全部写完

write系统调用实现位于/fs/read_write.c

SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf,
		size_t, count)
{
	/*struct fd {
	struct file *file;
	unsigned int flags;
	};
	*/
	//得到要操作的文件
	struct fd f = fdget_pos(fd);
	ssize_t ret = -EBADF;

	//如果文件fd对应的文件不存在,直接返回
	if (f.file) {
		//需要写文件的位置
		loff_t pos = file_pos_read(f.file);
		//调用vfs(虚拟文件系统) 提供的写函数,ret表示成功写入的数据字节大小
		ret = vfs_write(f.file, buf, count, &pos);
		//如果写入成功,就需要更改文件的操作(写入)位置
		if (ret >= 0)
			file_pos_write(f.file, pos);
		fdput_pos(f);
	}

	return ret;
}

2、vfs层进行数据传输

接着会调用vfs提供的写入函数

ssize_t vfs_write(struct file *file, const char __user *buf, size_t count, loff_t *pos)
{
	ssize_t ret;
	//判断当前的文件是否有写权限  f_mode存放了文件的读写权限,类似的可以用if (file->f_mode & FMODE_READ)判断文件是否有读权限
	if (!(file->f_mode & FMODE_WRITE))
		return -EBADF;
	if (!(file->f_mode & FMODE_CAN_WRITE))
		return -EINVAL;
	//检查用户空间缓冲区是否可访问
	if (unlikely(!access_ok(VERIFY_READ, buf, count)))
		return -EFAULT;
	
	//验证要写入的区域是否有效。如果验证失败,`ret` 将不为零
	ret = rw_verify_area(WRITE, file, pos, count);
	if (!ret) {
		//用户写入的数据最大为MAX_RW_COUNT,因此write不保证一次性都能将用户数据写入完成
		if (count > MAX_RW_COUNT)
			count =  MAX_RW_COUNT;
		file_start_write(file);
		ret = __vfs_write(file, buf, count, pos);
		if (ret > 0) {
			fsnotify_modify(file);
			add_wchar(current, ret);
		}
		inc_syscw(current);
		file_end_write(file);
	}

	return ret;
}


ssize_t __vfs_write(struct file *file, const char __user *p, size_t count,
		    loff_t *pos)
{
	if (file->f_op->write)
		return file->f_op->write(file, p, count, pos);
	else if (file->f_op->write_iter)
		return new_sync_write(file, p, count, pos);
	else
		return -EINVAL;
}

如果文件中的f_op存在write,就会调用write,否则如果存在write_iter,就会调用new_sync_write。
那么f_op是什么呢?

在这里插入图片描述

在这里插入图片描述

其实每个文件都对应着自己的file_operations,只有实现了里面的这些函数,文件才能进行相应的操作。举个例子,比如epoll,是所有的文件都能加入到epoll,让内核帮我们等待吗?当然不是,只有文件的file_operations实现了poll函数才能放到epoll中等待。

换句话讲,就是如果file_operations没有实现write或者write_iter,那么文件就无法写入,即使这个文件有读写权限也不行。

file_operations也体现了Linux下一切皆文件的含义。

对于socket文件,file_operations在net/socket.c中初始化的

static const struct file_operations socket_file_ops = {
	.owner =	THIS_MODULE,
	.llseek =	no_llseek,
	.read_iter =	sock_read_iter,
	.write_iter =	sock_write_iter,
	.poll =		sock_poll,
	.unlocked_ioctl = sock_ioctl,
#ifdef CONFIG_COMPAT
	.compat_ioctl = compat_sock_ioctl,
#endif
	.mmap =		sock_mmap,
	.release =	sock_close,
	.fasync =	sock_fasync,
	.sendpage =	sock_sendpage,
	.splice_write = generic_splice_sendpage,
	.splice_read =	sock_splice_read,
};

socket_file_ops 中没有write,却有write_iter(sock_write_iter),但后面却调用new_sync_write函数
但是不用担心,最终还是会调用到sock_write_iter函数

static ssize_t new_sync_write(struct file *filp, const char __user *buf, size_t len, loff_t *ppos)
{
	/*struct iovec{
	void __user *iov_base;	//缓存区
	__kernel_size_t iov_len; //缓冲区的大小
	};*/
	//将用户缓冲区赋值给struct iovec,这个结构体可以用于多缓冲区的写入(writev)和读取(readv),以减少系统调用,提高效率。 用户层可以传入多个缓冲区
	struct iovec iov = { .iov_base = (void __user *)buf, .iov_len = len };
	
	//struct kiocb通常用于 Linux 的异步 I/O 操作,当IO操作完成时,ki_complete函数将被调用
	/*struct kiocb {
	struct file		*ki_filp; //文件
	loff_t			ki_pos;  //偏移量
	void (*ki_complete)(struct kiocb *iocb, long ret, long ret2); //回调函数
	void			*private; //用于存储与特定异步 I/O 操作相关的私有数据
	int			ki_flags; //用于存储与异步 I/O 操作相关的各种标志
	};*/
	struct kiocb kiocb;

	//用于处理 I/O 向量(I/O vectors)的结构体。I/O 向量是一种用于表示不连续内存区域的数据结构,
	//struct iov_iter 提供了迭代和遍历这些向量的方法。
	struct iov_iter iter;
	ssize_t ret;

	//初始化上述的结构体
	init_sync_kiocb(&kiocb, filp);
	kiocb.ki_pos = *ppos;
	iov_iter_init(&iter, WRITE, &iov, 1, len);

	//调用write_iter(sock_write_iter)写入数据
	ret = filp->f_op->write_iter(&kiocb, &iter);
	BUG_ON(ret == -EIOCBQUEUED);
	if (ret > 0)
		*ppos = kiocb.ki_pos;
	return ret;
}

3、socket层进行数据传输

位于net/socket.c

static ssize_t sock_write_iter(struct kiocb *iocb, struct iov_iter *from)
{
	//获取文件
	struct file *file = iocb->ki_filp;
	//获取文件对应的socket。在创建socket时,会创建对应的file,并将socket的指针放在file的private_data
	struct socket *sock = file->private_data;
	//这个结构体用于封装要发送的消息。这里将 iov_iter 和 kiocb 传递给 msg
	struct msghdr msg = {.msg_iter = *from,
			     .msg_iocb = iocb};
	ssize_t res;

	if (iocb->ki_pos != 0)
		return -ESPIPE;

	if (file->f_flags & O_NONBLOCK)
		msg.msg_flags = MSG_DONTWAIT;

	if (sock->type == SOCK_SEQPACKET)
		msg.msg_flags |= MSG_EOR;

	//进一步调用sock_sendmsg
	res = sock_sendmsg(sock, &msg);
	*from = msg.msg_iter;
	return res;
}

int sock_sendmsg(struct socket *sock, struct msghdr *msg)
{
	int err = security_socket_sendmsg(sock, msg,
					  msg_data_left(msg));
	//进一步调用sock_sendmsg_nosec
	return err ?: sock_sendmsg_nosec(sock, msg);
}

static inline int sock_sendmsg_nosec(struct socket *sock, struct msghdr *msg)
{
	//调用sock->ops->sendmsg
	int ret = sock->ops->sendmsg(sock, msg, msg_data_left(msg));
	BUG_ON(ret == -EIOCBQUEUED);
	return ret;
}

这里的sock->ops在协议栈初始化时就已经确定了,指向了net/ipv4/af_inet.c中的inet_stream_ops

static struct inet_protosw inetsw_array[] =
{
	{
		.type =       SOCK_STREAM,
		.protocol =   IPPROTO_TCP,
		.prot =       &tcp_prot,
		.ops =        &inet_stream_ops,
		.flags =      INET_PROTOSW_PERMANENT |
			      INET_PROTOSW_ICSK,
	},
	...
}
const struct proto_ops inet_stream_ops = {
	...
	.sendmsg	   = inet_sendmsg,
	.recvmsg	   = inet_recvmsg,
	...
};
EXPORT_SYMBOL(inet_stream_ops);

其实调用的就是af_inet.c中的inet_sendmsg

int inet_sendmsg(struct socket *sock, struct msghdr *msg, size_t size)
{
	struct sock *sk = sock->sk;

	sock_rps_record_flow(sk);

	/* We may need to bind the socket. */
	if (!inet_sk(sk)->inet_num && !sk->sk_prot->no_autobind &&
	    inet_autobind(sk))
		return -EAGAIN;

	return sk->sk_prot->sendmsg(sk, msg, size);
}

接着调用sock中sk_prot的sendmsg
sk_prot也是在协议栈初始化时就已经确定了,指向了net/ipv4/af_inet.c中tcp_prot

struct proto tcp_prot = {
	...
	.recvmsg		= tcp_recvmsg,
	.sendmsg		= tcp_sendmsg,
	...
};

最终调用了tcp_sendmsg函数

4、tcp层进行数据传输

int tcp_sendmsg(struct sock *sk, struct msghdr *msg, size_t size)
{
	struct tcp_sock *tp = tcp_sk(sk);
	struct sk_buff *skb;
	struct sockcm_cookie sockc;
	int flags, err, copied = 0;
	int mss_now = 0, size_goal, copied_syn = 0;
	bool process_backlog = false;
	bool sg;
	long timeo;

	//对这个socket加锁
	lock_sock(sk);
	
	flags = msg->msg_flags;
	if ((flags & MSG_FASTOPEN) && !tp->repair) {
		//使用tcp fastopen来发送数据,允许客户端在SYN包中携带应用数据
		err = tcp_sendmsg_fastopen(sk, msg, &copied_syn, size);
		if (err == -EINPROGRESS && copied_syn > 0)
			goto out;
		else if (err)
			goto out_err;
	}

	//计算超时时间,如果设置了MSG_DONTWAIT标记,则超时时间为0
	timeo = sock_sndtimeo(sk, flags & MSG_DONTWAIT);

	//检测TCP连接是否受到应用层限制的
	tcp_rate_check_app_limited(sk);  /* is sending application-limited? */

	//只有ESTABLISHED和CLOSE_WAIT两个状态可以发送数据
	//CLOSE_WAIT是收到对端FIN但是本端还没有发送FIN时所处状态,所以也可以发送数据
	//TCP快速打开(被动端),它允许在连接完全建立之前发送数据
	//除了上述的其他状态都需要等待连接完成,才能传输数据
	if (((1 << sk->sk_state) & ~(TCPF_ESTABLISHED | TCPF_CLOSE_WAIT)) &&
	    !tcp_passive_fastopen(sk)) {
		err = sk_stream_wait_connect(sk, &timeo);
		if (err != 0)
			goto do_error;
	}

	if (unlikely(tp->repair)) {
		if (tp->repair_queue == TCP_RECV_QUEUE) {
			copied = tcp_send_rcvq(sk, msg, size);
			goto out_nopush;
		}

		err = -EINVAL;
		if (tp->repair_queue == TCP_NO_QUEUE)
			goto out_err;

	}

	sockc.tsflags = sk->sk_tsflags;
	if (msg->msg_controllen) {
		err = sock_cmsg_send(sk, msg, &sockc);
		if (unlikely(err)) {
			err = -EINVAL;
			goto out_err;
		}
	}

	/* This should be in poll */
	sk_clear_bit(SOCKWQ_ASYNC_NOSPACE, sk);

	//copied将记录本次能够写入TCP的字节数,如果成功,最终会返回给应用,初始化为0
	copied = 0;

restart:
	//每次发送都操作都会重新获取MSS值,保存到mss_now中
	mss_now = tcp_send_mss(sk, &size_goal, flags);

	err = -EPIPE;
	//检查之前TCP连接是否发生过异常
	if (sk->sk_err || (sk->sk_shutdown & SEND_SHUTDOWN))
		goto do_error;

	sg = !!(sk->sk_route_caps & NETIF_F_SG);

	//msg里保存着用户传入一个或者多个缓冲区,而msg_data_left(msg)返回的就是缓冲区数据量的大小
	while (msg_data_left(msg)) {
		int copy = 0;
		int max = size_goal;

		//获取发送队列中最后一个数据块,因为该数据块当前已保存数据可能还没有超过
		//size_goal,所以可以继续往该数据块中填充数据
		skb = tcp_write_queue_tail(sk);

		//tcp_send_head()返回sk_send_head,指向发送队列中下一个要发送的数据包
		//sk_send_head如果为NULL表示待发送的数据为空(可能有待确认数据)
		//如果不为NULL,copy则表示还能往这个skb放入多少数据
		if (tcp_send_head(sk)) {
			if (skb->ip_summed == CHECKSUM_NONE)
				max = mss_now;
			copy = max - skb->len;
		}

		//copy <= 0说明发送队列最后一个skb数据量也达到了size_goal,不能继续填充数据了
		if (copy <= 0 || !tcp_skb_can_collapse_to(skb)) {
			bool first_skb;

new_segment:
			//分配新的skb
			//即将分配内存,首先检查内存使用是否会超限,如果会要先等待有内存可用
			if (!sk_stream_memory_free(sk))
				goto wait_for_sndbuf;

			if (process_backlog && sk_flush_backlog(sk)) {
				process_backlog = false;
				goto restart;
			}
			//判断即将申请的skb是否是发送队列的第一个skb
			first_skb = skb_queue_empty(&sk->sk_write_queue);
			//申请skb
			//分配skb,select_size()的返回值决定了skb的线性区域大小
			skb = sk_stream_alloc_skb(sk,
						  select_size(sk, sg, first_skb),
						  sk->sk_allocation,
						  first_skb);
			//分配失败,需要等待有剩余内存可用后才能继续发送
			if (!skb)
				goto wait_for_memory;

			process_backlog = true;
			//根据硬件能力确定TCP是否需要执行校验工作
			if (sk_check_csum_caps(sk))
				skb->ip_summed = CHECKSUM_PARTIAL;

			//将新分配的skb加入到TCB的发送队列中,并且更新相关内存记账信息
			skb_entail(sk, skb);

			//设置本轮要拷贝的数据量为size_goal,因为该skb是新分配的,所以
			//一定可以容纳这么多,但是具体能不能拷贝这么多,还需要看有没有这么
			//多的数据要发送,
			copy = size_goal;
			max = size_goal;

			if (tp->repair)
				TCP_SKB_CB(skb)->sacked |= TCPCB_REPAIRED;
		}

		/* Try to append data to the end of skb. */
		//如果skb可拷贝的数据量(copy)大于用户传入的数据量,那么就可以一次性全部拷贝完
		if (copy > msg_data_left(msg))
			copy = msg_data_left(msg);

		//如果skb的线性部分还有空间,先填充这部分
		if (skb_availroom(skb) > 0) {
			/* We have some space in skb head. Superb! */
			//如果线性空间部分小于当前要拷贝的数据量,则调整本轮要拷贝的数据量
			copy = min_t(int, copy, skb_availroom(skb));
			//拷贝数据,如果出错则结束发送过程
			err = skb_add_data_nocache(sk, skb, &msg->msg_iter, copy);
			if (err)
				goto do_fault;
		} else {
			//merge用于指示是否可以将新拷贝的数据和当前skb的最后一个片段合并。如果
			//它们在页面内刚好是连续的,那么就可以合并为一个片段
			bool merge = true;
			//i为当前skb中已经存在的分片个数
			int i = skb_shinfo(skb)->nr_frags;
			//page指向上一次分配的页面,off指向该页面中的偏移量
			struct page_frag *pfrag = sk_page_frag(sk);

			if (!sk_page_frag_refill(sk, pfrag))
				goto wait_for_memory;

			//该函数用于判断该skb最后一个片段是否就是当前页面的最后一部分,如果是,那么新拷贝的
			//数据和该片段就可以合并,所以设置merge为1,这样可以节省一个frag_list[]位置
			if (!skb_can_coalesce(skb, i, pfrag->page,
					      pfrag->offset)) {
				if (i >= sysctl_max_skb_frags || !sg) {
					tcp_mark_push(tp, skb);
					goto new_segment;
				}
				merge = false;
			}

			copy = min_t(int, copy, pfrag->size - pfrag->offset);

			if (!sk_wmem_schedule(sk, copy))
				goto wait_for_memory;

			//拷贝copy字节数据到页面中
			err = skb_copy_to_page_nocache(sk, &msg->msg_iter, skb,
						       pfrag->page,
						       pfrag->offset,
						       copy);
			if (err)
				goto do_error;

			//更新skb中相关指针、计数信息
			if (merge) {
				skb_frag_size_add(&skb_shinfo(skb)->frags[i - 1], copy);
			} else {
				skb_fill_page_desc(skb, i, pfrag->page,
						   pfrag->offset, copy);
				get_page(pfrag->page);
			}
			pfrag->offset += copy;
		}

		//如果本轮是第一次拷贝,清除PUSH标记
		if (!copied)
			TCP_SKB_CB(skb)->tcp_flags &= ~TCPHDR_PSH;

		//write_seq记录的是发送队列中下一个要分配的序号,所以这里需要更新它
		tp->write_seq += copy;
		//更新该数据包的最后一个字节的序号
		TCP_SKB_CB(skb)->end_seq += copy;
		tcp_skb_pcount_set(skb, 0);
		
		//累加已经拷贝字节数
		copied += copy;
		//如果所有要发送的数据都拷贝完了,并且设置了MSG_EOR,结束发送过程
		if (!msg_data_left(msg)) {
			if (unlikely(flags & MSG_EOR))
				TCP_SKB_CB(skb)->eor = 1;
			goto out;
		}

		//如果该skb没有填满,继续下一轮拷贝
		if (skb->len < max || (flags & MSG_OOB) || unlikely(tp->repair))
			continue;

		//如果需要设置PUSH标志位,那么设置PUSH,然后发送数据包,可将PUSH可以让TCP尽快的发送数据
		if (forced_push(tp)) {
			tcp_mark_push(tp, skb);
			//尽可能的将发送队列中的skb发送出去,禁用nalge
			__tcp_push_pending_frames(sk, mss_now, TCP_NAGLE_PUSH);
		} else if (skb == tcp_send_head(sk))
			//当前只有这一个skb,也发送出去。因为只有一个,所以肯定也不存在拥塞,可以发送
			tcp_push_one(sk, mss_now);
		continue;

wait_for_sndbuf:
		//设置套接字结构中发送缓存不足的标志
		set_bit(SOCK_NOSPACE, &sk->sk_socket->flags);
wait_for_memory:
		//如果已经有数据拷贝到了发送缓存中,那么调用tcp_push()立即发送,这样可能可以
		//让发送缓存快速的有剩余空间可用
		if (copied)
			tcp_push(sk, flags & ~MSG_MORE, mss_now,
				 TCP_NAGLE_PUSH, size_goal);

		//等待有空余内存可以使用,如果timeo不为0,那么这一步会休眠
		err = sk_stream_wait_memory(sk, &timeo);
		if (err != 0)
			goto do_error;
		
		//睡眠后MSS可能发生了变化,所以重新计算
		mss_now = tcp_send_mss(sk, &size_goal, flags);
	}

out:
	//如果拷贝了数据到发送缓存区,尝试进行一次发送
	if (copied) {
		tcp_tx_timestamp(sk, sockc.tsflags, tcp_write_queue_tail(sk));
		tcp_push(sk, flags, mss_now, tp->nonagle, size_goal);
	}
out_nopush:
	release_sock(sk);
	//返回本次写入的数据量
	return copied + copied_syn;

do_fault:
	//发生了错误,并且当前skb尚未包含任何数据,那么需要释放该skb
	if (!skb->len) {
		tcp_unlink_write_queue(skb, sk);
		tcp_check_send_head(sk, skb);
		sk_wmem_free_skb(sk, skb);
	}

do_error:
	if (copied + copied_syn)
		goto out;
out_err:
	err = sk_stream_error(sk, flags, err);
	if (unlikely(skb_queue_len(&sk->sk_write_queue) == 0 && err == -EAGAIN))
		sk->sk_write_space(sk);
	release_sock(sk);
	return err;
}

tcp_sendmsg主要做了以下几件事:
1.判断套接字状态
2.将用户数据拷贝到skb中,优先考虑报文的线性区,然后是分页区,必要时需要使用新skb或者新分页来存放用户数据
3.根据具体的情况调用__tcp_push_pending_frames(sk, mss_now, TCP_NAGLE_PUSH)(禁用了nagle算法,可以发送多个skb)或者tcp_push_one(sk, mss_now)(使用nagle算法,只能发送一个skb)发送数据

无论是调用上面的哪个函数发送数据,但最终都会调用到tcp_write_xmit函数

static bool tcp_write_xmit(struct sock *sk, unsigned int mss_now, int nonagle,
			   int push_one, gfp_t gfp)
{
	struct tcp_sock *tp = tcp_sk(sk);
	struct sk_buff *skb;
	unsigned int tso_segs, sent_pkts;
	int cwnd_quota;
	int result;
	bool is_cwnd_limited = false;
	u32 max_segs;

	//sent_pkts用来统计函数中已发送报文总数
	sent_pkts = 0;

	//检查是不是只发送一个skb buffer,即push one
	if (!push_one) {
		//执行MTU探测
		result = tcp_mtu_probe(sk);
		if (!result) {
			return false;
		} else if (result > 0) {
			sent_pkts = 1;
		}
	}

	max_segs = tcp_tso_segs(sk, mss_now);// 计算最大可发送的段数
	while ((skb = tcp_send_head(sk))) {// 遍历发送队列
		unsigned int limit;

		/* 设置有关TSO的信息,包括GSO类型,GSO分段的大小等等。
		 * 这些信息是准备给软件TSO分段使用的。
		 * 如果网络设备不支持TSO,但又使用了TSO功能,
		 * 则报文在提交给网络设备之前,需进行软分段,即由代码实现TSO分段。
		 */
		tso_segs = tcp_init_tso_segs(skb, mss_now);
		BUG_ON(!tso_segs);

		if (unlikely(tp->repair) && tp->repair_queue == TCP_SEND_QUEUE) {
			skb_mstamp_get(&skb->skb_mstamp);
			goto repair; 
		}
		/* 检查congestion windows, 可以发送几个segment */
		/* 检测拥塞窗口的大小,如果为0,则说明拥塞窗口已满,目前不能发送。
         * 拿拥塞窗口和正在网络上传输的包数目相比,如果拥塞窗口还大,
         * 则返回拥塞窗口减掉正在网络上传输的包数目剩下的大小。
         * 该函数目的是判断正在网络上传输的包数目是否超过拥塞窗口,
         * 如果超过了,则不发送。
         */
		cwnd_quota = tcp_cwnd_test(tp, skb);
		if (!cwnd_quota) {	
			//push_one为2表示需要强制发送,此时就设置窗口大小为1,表示可以发送一个数据包
			if (push_one == 2)
				cwnd_quota = 1;
			else
				break;
		}

		//检测当前报文是否完全处于发送窗口内,如果是则可以发送,否则不能发送
		if (unlikely(!tcp_snd_wnd_test(tp, skb, mss_now)))
			break;

		//tso_segs=1表示无需tso分段 
		if (tso_segs == 1) {
			//根据nagle算法,计算是否需要发送数据
			if (unlikely(!tcp_nagle_test(tp, skb, mss_now,
						     (tcp_skb_is_last(sk, skb) ?
						      nonagle : TCP_NAGLE_PUSH))))
				break;
		} else {
			/* 当不止一个skb时,通过TSO计算是否需要延时发送 */
	        /* 如果需要TSO分段,则检测该报文是否应该延时发送。
	         * tcp_tso_should_defer()用来检测GSO段是否需要延时发送。
             * 在段中有FIN标志,或者不处于open拥塞状态,或者TSO段延时超过2个时钟滴答,
             * 或者拥塞窗口和发送窗口的最小值大于64K或三倍的当前有效MSS,在这些情况下会立即发送,
             * 而其他情况下会延时发送,这样主要是为了减少软GSO分段的次数,以提高性能。
             */
			if (!push_one &&
			    tcp_tso_should_defer(sk, skb, &is_cwnd_limited,
						 max_segs))
				break;
		}

		limit = mss_now;
		/* 在TSO分片大于1的情况下,且TCP不是URG模式。通过MSS计算发送数据的limit
		 * 以发送窗口和拥塞窗口的最小值作为分段段长*/
		if (tso_segs > 1 && !tcp_urg_mode(tp))
			limit = tcp_mss_split_point(sk, skb, mss_now,
						    min_t(unsigned int,
							  cwnd_quota,
							  max_segs),
						    nonagle);

		/* 当skb的长度大于限制时,需要调用tso_fragment分片,如果分段失败则暂不发送 */
		if (skb->len > limit &&
		    unlikely(tso_fragment(sk, skb, limit, mss_now, gfp)))
			break;
		//检查当前TCP发送队列的状态
		//它可能会考虑队列的长度、当前的网络条件、拥塞窗口的大小以及其他相关因素,
		//以确定是否应该继续发送数据或采取其他行动(如延迟发送)。这个函数的主要目的是避免队列过度拥塞,从而保持网络传输的稳定性和效率。
		if (tcp_small_queue_check(sk, skb, 0))
			break;
		//调用tcp_transmit_skb()发送TCP段,其中第三个参数1表示是否需要克隆被发送的报文
		if (unlikely(tcp_transmit_skb(sk, skb, 1, gfp)))
			break;

repair:
		/* 更新统计,并启动重传计时器 */
		/* 调用tcp_event_new_data_sent()-->tcp_advance_send_head()更新sk_send_head,
		 * 即取发送队列中的下一个SKB。同时更新snd_nxt,即等待发送的下一个TCP段的序号,
		 * 然后统计发出但未得到确认的数据报个数。最后如果发送该报文前没有需要确认的报文,
		 * 则复位重传定时器,对本次发送的报文做重传超时计时。
		 */
		tcp_event_new_data_sent(sk, skb);

		/* 更新struct tcp_sock中的snd_sml字段。snd_sml表示最近发送的小包(小于MSS的段)的最后一个字节序号,
		 * 在发送成功后,如果报文小于MSS,即更新该字段,主要用来判断是否启动nagle算法
		 */
		tcp_minshall_update(tp, mss_now, skb);
		sent_pkts += tcp_skb_pcount(skb);

		if (push_one)
			break;
	}

	/* 如果本次有数据发送,则对TCP拥塞窗口进行检查确认。*/
	if (likely(sent_pkts)) {
		if (tcp_in_cwnd_reduction(sk))
			tp->prr_out += sent_pkts;

		/* Send one loss probe per tail loss episode. */
		if (push_one != 2)
			//丢包检测
			tcp_schedule_loss_probe(sk);
		//更新拥塞控制状态
		is_cwnd_limited |= (tcp_packets_in_flight(tp) >= tp->snd_cwnd);
		//验证拥塞窗口
		tcp_cwnd_validate(sk, is_cwnd_limited);
		return false;
	}
	/* 
	 * 如果本次没有数据发送,则根据已发送但未确认的报文数packets_out和sk_send_head返回,
	 * packets_out不为零或sk_send_head为空都视为有数据发出,因此返回成功。
	 */
	return !tp->packets_out && tcp_send_head(sk);
}

接着会调用tcp_transmit_skb函数,填充tcp头部

static int tcp_transmit_skb(struct sock *sk, struct sk_buff *skb, int clone_it,
			    gfp_t gfp_mask)
{
	return __tcp_transmit_skb(sk, skb, clone_it, gfp_mask,
				  tcp_sk(sk)->rcv_nxt);
}


static int __tcp_transmit_skb(struct sock *sk, struct sk_buff *skb,
			      int clone_it, gfp_t gfp_mask, u32 rcv_nxt)
{
	const struct inet_connection_sock *icsk = inet_csk(sk);
	struct inet_sock *inet;
	struct tcp_sock *tp;
	struct tcp_skb_cb *tcb;
	struct tcp_out_options opts;
	unsigned int tcp_options_size, tcp_header_size;
	struct sk_buff *oskb = NULL;
	struct tcp_md5sig_key *md5;
	struct tcphdr *th;
	int err;

	BUG_ON(!skb || !tcp_skb_pcount(skb));
	tp = tcp_sk(sk);

	//根据传递进来的clone_it参数来确定是否需要克隆待发送的报文
	if (clone_it) {
		TCP_SKB_CB(skb)->tx.in_flight = TCP_SKB_CB(skb)->end_seq
			- tp->snd_una;
		oskb = skb;
		//如果skb已经被clone,则只能复制该skb的数据到新分配的skb中
		if (unlikely(skb_cloned(skb)))
			skb = pskb_copy(skb, gfp_mask);
		else
			//clone新的skb
			skb = skb_clone(skb, gfp_mask);
		if (unlikely(!skb))
			return -ENOBUFS;
	}
	skb_mstamp_get(&skb->skb_mstamp);

	//获取INET层和TCP层的传输控制块、skb中的TCP私有数据块
	inet = inet_sk(sk);
	tcb = TCP_SKB_CB(skb);
	memset(&opts, 0, sizeof(opts));

	/*根据TCP选项重新调整TCP首部的长度。*/
    /*判断当前TCP报文是否是SYN段,因为有些选项只能出现在SYN报文中,需做特别处理。*/
	if (unlikely(tcb->tcp_flags & TCPHDR_SYN))
		tcp_options_size = tcp_syn_options(sk, skb, &opts, &md5);
	else
		tcp_options_size = tcp_established_options(sk, skb, &opts,
							   &md5);
	/*tcp首部的总长度等于可选长度加上TCP头部。*/
	tcp_header_size = tcp_options_size + sizeof(struct tcphdr);
	skb->ooo_okay = sk_wmem_alloc_get(sk) < SKB_TRUESIZE(1);
	
	 /*调用skb_push()在数据部分的头部添加TCP首部,长度即为之前计算得到的那个tcp_header_size,实际上是把data指针往上移。*/
	skb_push(skb, tcp_header_size);
	skb_reset_transport_header(skb);

	skb_orphan(skb);
	// 将skb和sock关联起来,并设置skb的析构函数
	skb->sk = sk;
	skb->destructor = skb_is_tcp_pure_ack(skb) ? __sock_wfree : tcp_wfree;
	// 从sock中设定skb的哈希值
	skb_set_hash_from_sk(skb, sk);
	// 增加skb占用的内存大小计数
	atomic_add(skb->truesize, &sk->sk_wmem_alloc);

	//填充TCP首部中的源端口source、目的端口dest、TCP报文的序号seq、确认序号ack_seq以及各个标志位
	th = (struct tcphdr *)skb->data;
	th->source		= inet->inet_sport;
	th->dest		= inet->inet_dport;
	th->seq			= htonl(tcb->seq);
	th->ack_seq		= htonl(rcv_nxt);
	*(((__be16 *)th) + 6)	= htons(((tcp_header_size >> 2) << 12) |
					tcb->tcp_flags);

	th->check		= 0;
	th->urg_ptr		= 0;

	// 如果当前包含紧急指针的包在snd_una探测窗口之下,
    // 则需要设置紧急指针。
	if (unlikely(tcp_urg_mode(tp) && before(tcb->seq, tp->snd_up))) {
		if (before(tp->snd_up, tcb->seq + 0x10000)) {
			th->urg_ptr = htons(tp->snd_up - tcb->seq);
			th->urg = 1;
		} else if (after(tcb->seq + 0xFFFF, tp->snd_nxt)) {
			th->urg_ptr = htons(0xFFFF);
			th->urg = 1;
		}
	}

	//构建TCP选项,例如窗口大小,时间戳等选项
	tcp_options_write((__be32 *)(th + 1), tp, &opts);
	// 设置skb的GSO类型
	skb_shinfo(skb)->gso_type = sk->sk_gso_type;
	//分两种情况设置TCP首部的接收窗口的大小
	if (likely(!(tcb->tcp_flags & TCPHDR_SYN))) {
		//如果不是SYN报文,则调用tcp_select_window()计算当前接收窗口的大小
		th->window      = htons(tcp_select_window(sk));
		tcp_ecn_send(sk, skb, th, tcp_header_size);
	} else {
		//如果是SYN段,则设置接收窗口初始值为rcv_wnd
		th->window	= htons(min(tp->rcv_wnd, 65535U));
	}
#ifdef CONFIG_TCP_MD5SIG
	// 如果启用了MD5签名,计算MD5哈希,因为我们现在有了所需的全部数据
	if (md5) {
		sk_nocaps_add(sk, NETIF_F_GSO_MASK);
		tp->af_specific->calc_md5_hash(opts.hash_location,
					       md5, sk, skb);
	}
#endif

	// 由底层网络函数完成skb的校验和
	icsk->icsk_af_ops->send_check(sk, skb);

 	//如果skb中设置了TCPHDR_ACK标志,则记录ACK已发送的事件
	if (likely(tcb->tcp_flags & TCPHDR_ACK))
		tcp_event_ack_sent(sk, tcp_skb_pcount(skb), rcv_nxt);

	// 如果skb的长度不只是TCP头部的长度,表示有数据被发送,
    // 更新统计信息。
	if (skb->len != tcp_header_size) {
		tcp_event_data_sent(tp, sk);
		tp->data_segs_out += tcp_skb_pcount(skb);
	}

	// 如果当前数据段的结束序列号在snd_nxt之后或与之相等,
    // 或者如果是一个单独的序列号,更新发送的数据包统计。
	if (after(tcb->end_seq, tp->snd_nxt) || tcb->seq == tcb->end_seq)
		TCP_ADD_STATS(sock_net(sk), TCP_MIB_OUTSEGS,
			      tcp_skb_pcount(skb));

	tp->segs_out += tcp_skb_pcount(skb);
	// 设置skb中GSO(分段卸载)相关的字段
	skb_shinfo(skb)->gso_segs = tcp_skb_pcount(skb);
	skb_shinfo(skb)->gso_size = tcp_skb_mss(skb);

	// 我们使用的时间戳应保持私有。
	skb->tstamp.tv64 = 0;

	// 清理我们对IP栈的"痕迹",重置skb的控制块。
	memset(skb->cb, 0, max(sizeof(struct inet_skb_parm),
			       sizeof(struct inet6_skb_parm)));

	//调用发送接口queue_xmit发送报文,进入到ip层,如果失败返回错误码。在TCP中该接口实现函数为ip_queue_xmit()
	err = icsk->icsk_af_ops->queue_xmit(sk, skb, &inet->cork.fl);

	// 如果出现错误,调用tcp_enter_cwr函数进入拥塞窗口减少状态,并评估错误。
	if (unlikely(err > 0)) {
		tcp_enter_cwr(sk);
		err = net_xmit_eval(err);
	}
	// 如果发送成功并且有原始的skb,更新相关统计数据。
	if (!err && oskb) {
		skb_mstamp_get(&oskb->skb_mstamp);
		tcp_rate_skb_sent(sk, oskb);
	}
	return err;
}

5、ip层进行数据传输

icsk->icsk_af_ops->queue_xmit(sk, skb, &inet->cork.fl)函数就会调用到ip_output.c中的ip_queue_xmit函数,主要就是获取路由,填充ip头部信息

int ip_queue_xmit(struct sock *sk, struct sk_buff *skb, struct flowi *fl)
{
	struct inet_sock *inet = inet_sk(sk);
	struct net *net = sock_net(sk);
	struct ip_options_rcu *inet_opt;
	struct flowi4 *fl4; //这个结构体帮助内核处理IPv4网络数据包,包括路由选择、策略路由、流量控制等
	struct rtable *rt; //这个结构体包含了关于特定路由条目的各种信息,例如目标地址、下一跳网关、网络接口等
	struct iphdr *iph;
	int res;

	rcu_read_lock();
	
	inet_opt = rcu_dereference(inet->inet_opt);
	fl4 = &fl->u.ip4;
	//如果还没有查询过路由,那么就先查询路由。对于TCP,大多数情况下都已经查询过了
	//先从skb中查找路由信息
	rt = skb_rtable(skb);
	if (rt)
		goto packet_routed;

	//路由和套接字是关联的,一般来讲,一旦查询后,目的地址不发生变化,路由查询结果
	//不会有变化,所以往往会将路由查询结果缓存到sk中,上面发现skb->dst中没有设置,
	//再检查sk中缓存的路由信息是否依然有效,如果也无效,那么向路由子系统发起查询
	rt = (struct rtable *)__sk_dst_check(sk, 0);
	if (!rt) {//如果路由缓存项过期,则重新通过输出网络设备dev,目的地址,源地址等信息查找输出路由缓存项。
			  //如果查找到对应的路由缓存项,则将其缓存到输出控制块中,否则丢弃该数据包
		__be32 daddr;

		//如果有源路由选项,在查路由之前替换下目的地址
		daddr = inet->inet_daddr;
		if (inet_opt && inet_opt->opt.srr)
			daddr = inet_opt->opt.faddr;

		//根据这些参数查找路由信息
		rt = ip_route_output_ports(net, fl4, sk,
					   daddr, inet->inet_saddr,
					   inet->inet_dport,
					   inet->inet_sport,
					   sk->sk_protocol,
					   RT_CONN_FLAGS(sk),
					   sk->sk_bound_dev_if);
		if (IS_ERR(rt))
			goto no_route;
		sk_setup_caps(sk, &rt->dst);
	}
	skb_dst_set_noref(skb, &rt->dst);//如果没有过期则使用缓存再传输控制块中的路由缓存项

packet_routed:
	//查找到输出路由以后,先进行严格源路由选项的处理,如果存在严格源路由选项,并且路由使用网关地址,则丢弃
	if (inet_opt && inet_opt->opt.is_strictroute && rt->rt_uses_gateway)
		goto no_route;

	skb_push(skb, sizeof(struct iphdr) + (inet_opt ? inet_opt->opt.optlen : 0));//现在只要要往哪里发送了,申请并创建IP头部
	skb_reset_network_header(skb); //重新设置网络层头部指针skb->network_header
	iph = ip_hdr(skb);
	*((__be16 *)iph) = htons((4 << 12) | (5 << 8) | (inet->tos & 0xff));
	if (ip_dont_fragment(sk, &rt->dst) && !skb->ignore_df)//如果有设置不需要分片,设置DF标记位,否则初始化frag_off为0
		iph->frag_off = htons(IP_DF);
	else
		iph->frag_off = 0;
	iph->ttl      = ip_select_ttl(inet, &rt->dst);//设置IP头部的TTL
	iph->protocol = sk->sk_protocol;//设置IP头部的协议
	//设置源IP和目的IP
	ip_copy_addrs(iph, fl4);

	/* Transport layer set skb->h.foo itself. */

	//如果有选项,则需要给IP头部添加选项部分
	if (inet_opt && inet_opt->opt.optlen) {
		iph->ihl += inet_opt->opt.optlen >> 2;
		//重新调整了IP首部长度,加上了选项部分的长度
		ip_options_build(skb, &inet_opt->opt, inet->inet_daddr, rt, 0);
	}

	ip_select_ident_segs(net, skb, sk,
			     skb_shinfo(skb)->gso_segs ?: 1);

	skb->priority = sk->sk_priority;
	skb->mark = sk->sk_mark;

	//对于单播包使用的是ip_output,多播使用的是ip_mc_output
	res = ip_local_out(net, sk, skb);
	rcu_read_unlock();
	return res;

no_route:
	rcu_read_unlock();
	IP_INC_STATS(net, IPSTATS_MIB_OUTNOROUTES);
	kfree_skb(skb);
	return -EHOSTUNREACH;
}

接着会调用ip_local_out函数

int ip_local_out(struct net *net, struct sock *sk, struct sk_buff *skb)
{
	int err;

	err = __ip_local_out(net, sk, skb);
	if (likely(err == 1))
		err = dst_output(net, sk, skb);

	return err;
}

在ip_local_out内又会调用__ip_local_out或者dst_output,如果调用了__ip_local_out,在它内部还是会调用到dst_output函数

int __ip_local_out(struct net *net, struct sock *sk, struct sk_buff *skb)
{
	// 获取指向sk_buff中IP报头的指针
	struct iphdr *iph = ip_hdr(skb);

	// 设置IP报头中的总长度字段为skb的长度,htons用于将主机字节顺序转换为网络字节顺序
	iph->tot_len = htons(skb->len);
	// 计算和填充IP报头的校验和
	ip_send_check(iph);

	/* 如果出口设备属于一个L3主设备,就将skb传递给它的处理函数
     * l3mdev_ip_out负责处理skb,可能进行一些特定于该设备的处理
     */
	skb = l3mdev_ip_out(sk, skb);
	// 如果skb为空,说明处理不成功,返回0
	if (unlikely(!skb))
		return 0;

	// 设置skb的协议字段为IP协议,htons用于将主机字节顺序转换为网络字节顺序
	skb->protocol = htons(ETH_P_IP);

	// 调用netfilter钩子,以便进行进一步的处理(例如,过滤,NAT等)
    // nf_hook会根据配置决定是否处理skb或将其传递给下一个处理阶段
	return nf_hook(NFPROTO_IPV4, NF_INET_LOCAL_OUT,
		       net, sk, skb, NULL, skb_dst(skb)->dev,
		       dst_output);
}


static inline int dst_output(struct net *net, struct sock *sk, struct sk_buff *skb)
{
	return skb_dst(skb)->output(net, sk, skb);
}

这里会调用到output函数,这里的output就是ip_output函数

int ip_output(struct net *net, struct sock *sk, struct sk_buff *skb)
{
	//获取网络设备
	struct net_device *dev = skb_dst(skb)->dev;

	//更新输出的统计信息
	IP_UPD_PO_STATS(net, IPSTATS_MIB_OUT, skb->len);
	//设置网络设备和协议
	skb->dev = dev;
	skb->protocol = htons(ETH_P_IP);

	//如果 Netfilter 钩子函数没有返回 NF_DROP(表示丢弃数据包),
	//那么 ip_finish_output 函数最终会被调用,以完成数据包的发送。
	return NF_HOOK_COND(NFPROTO_IPV4, NF_INET_POST_ROUTING,
			    net, sk, skb, NULL, dev,
			    ip_finish_output,
			    !(IPCB(skb)->flags & IPSKB_REROUTED));
}

紧接着会调用ip_finish_output

static int ip_finish_output(struct net *net, struct sock *sk, struct sk_buff *skb)
{
	unsigned int mtu;

#if defined(CONFIG_NETFILTER) && defined(CONFIG_XFRM)
	/* Policy lookup after SNAT yielded a new policy */
	if (skb_dst(skb)->xfrm) {
		IPCB(skb)->flags |= IPSKB_REROUTED;
		return dst_output(net, sk, skb);
	}
#endif
	//获取数据包的目的地 MTU
	mtu = ip_skb_dst_mtu(sk, skb);
	//如果数据包是巨型帧,就使用ip_finish_output_gso来处理
	if (skb_is_gso(skb))
		return ip_finish_output_gso(net, sk, skb, mtu);

	//skb的长度大于对端的mtu,或者设置了IPSKB_FRAG_PMTU,就会调用ip_fragment进行分片
	//分完片后再调用ip_finish_output2
	if (skb->len > mtu || (IPCB(skb)->flags & IPSKB_FRAG_PMTU))
		return ip_fragment(net, sk, skb, mtu, ip_finish_output2);

	return ip_finish_output2(net, sk, skb);
} 

然后会调用ip_finish_output2函数

static int ip_finish_output2(struct net *net, struct sock *sk, struct sk_buff *skb)
{
	struct dst_entry *dst = skb_dst(skb);
	struct rtable *rt = (struct rtable *)dst;
	struct net_device *dev = dst->dev;
	unsigned int hh_len = LL_RESERVED_SPACE(dev);
	struct neighbour *neigh;
	u32 nexthop;
	
	//如果与此数据包关联的路由是多播类型,则使用 IP_UPD_PO_STATS 宏来增加 OutMcastPkts 和 OutMcastOctets 计数
	if (rt->rt_type == RTN_MULTICAST) {
		IP_UPD_PO_STATS(net, IPSTATS_MIB_OUTMCAST, skb->len);
	} else if (rt->rt_type == RTN_BROADCAST)//如果广播路由,则会增加 OutBcastPkts 和 OutBcastOctets 计数。
		IP_UPD_PO_STATS(net, IPSTATS_MIB_OUTBCAST, skb->len);

	//确保 skb 结构有足够的空间容纳需要添加的任何链路层头
	if (unlikely(skb_headroom(skb) < hh_len && dev->header_ops)) {
		struct sk_buff *skb2;
		//确保 skb 结构有足够的空间容纳需要添加的任何链路层头
		skb2 = skb_realloc_headroom(skb, LL_RESERVED_SPACE(dev));
		if (!skb2) {
			kfree_skb(skb);
			return -ENOMEM;
		}
		if (skb->sk)
			skb_set_owner_w(skb2, skb->sk);
		consume_skb(skb);
		skb = skb2;
	}

	if (lwtunnel_xmit_redirect(dst->lwtstate)) {
		int res = lwtunnel_xmit(skb);

		if (res < 0 || res == LWTUNNEL_XMIT_DONE)
			return res;
	}

	rcu_read_lock_bh();
	//查询路由层找到下一跳的ip地址
	nexthop = (__force u32) rt_nexthop(rt, ip_hdr(skb)->daddr);
	//再根据下一跳的ip地址查找邻居缓存
	neigh = __ipv4_neigh_lookup_noref(dev, nexthop);
	if (unlikely(!neigh))
		//如果未找到,则调用__neigh_create 创建一个邻居,内部就是ARP相关的操作
		neigh = __neigh_create(&arp_tbl, &nexthop, dev, false);
	if (!IS_ERR(neigh)) {
		//调用 dst_neigh_output 继续传递 skb
		int res = dst_neigh_output(dst, neigh, skb);

		rcu_read_unlock_bh();
		return res;
	}
	rcu_read_unlock_bh();

	net_dbg_ratelimited("%s: No header cache and no neighbour!\n",
			    __func__);
	kfree_skb(skb);
	return -EINVAL;
}

紧接着会调用到dst_neigh_output函数

static inline int dst_neigh_output(struct dst_entry *dst, struct neighbour *n,
				   struct sk_buff *skb)
{
	const struct hh_cache *hh;

	//如果路由条目 dst 有一个待确认的标志,则将其清除,并更新邻居 n 的确认时间戳
	if (dst->pending_confirm) {
		unsigned long now = jiffies;

		dst->pending_confirm = 0;
		if (n->confirmed != now)
			n->confirmed = now;
	}

	//获取下一跳ip地址所对应的硬件头信息(内部包含mac地址)
	hh = &n->hh;
	//如果邻居处于已连接状态(NUD_CONNECTED)并且硬件头缓存中有有效的数据,则调用neigh_hh_output发送数据包
	if ((n->nud_state & NUD_CONNECTED) && hh->hh_len)
		return neigh_hh_output(hh, skb);
	else //否则调用邻居条目 n 中的 output 方法来发送数据包
		return n->output(n, skb);
}

以上两种情况,最后都会到 dev_queue_xmit,它将 skb 发送给 Linux 网络设备子系统,在它进入设备驱动程序层之前将对其进行更多处理。让我们沿着 neigh_hh_output 和 n->output 代码继续向下,直到达到 dev_queue_xmit

neigh_hh_output函数

static inline int neigh_hh_output(const struct hh_cache *hh, struct sk_buff *skb)
{
	unsigned int seq;
	int hh_len;
	
	//填充mac地址
	do {
		seq = read_seqbegin(&hh->hh_lock);
		hh_len = hh->hh_len;
		if (likely(hh_len <= HH_DATA_MOD)) {
			memcpy(skb->data - HH_DATA_MOD, hh->hh_data, HH_DATA_MOD);
		} else {
			int hh_alen = HH_DATA_ALIGN(hh_len);

			memcpy(skb->data - hh_alen, hh->hh_data, hh_alen);
		}
	} while (read_seqretry(&hh->hh_lock, seq));

	//更新 skb 内指向数据缓冲区的指针和数据长度
	skb_push(skb, hh_len);
	//调用 dev_queue_xmit 将 skb 传递给 Linux 网络设备子系统
	return dev_queue_xmit(skb);
}

如果邻居已经关闭,则会调用n->output,也就是struct neighbour中的output,这里的output就是neigh_ops中的output,而neigh_ops的初始化又是在net/ipv4/arp.c中

static const struct neigh_ops arp_generic_ops = {
	.family =		AF_INET,
	.solicit =		arp_solicit,
	.error_report =		arp_error_report,
	.output =		neigh_resolve_output,
	.connected_output =	neigh_connected_output,
};

最终调用的是neigh_resolve_output

int neigh_resolve_output(struct neighbour *neigh, struct sk_buff *skb)
{
	int rc = 0;

	//neigh_event_send() 函数发送一个查询邻居节点 MAC 地址的 ARP 请求
	//在这个函数内部会把skb添加到邻居节点信息对象的 arp_queue 队列中,等待获取到邻居节点 MAC 地址后重新发送这个数据包
	if (!neigh_event_send(neigh, skb)) {
		int err;
		struct net_device *dev = neigh->dev;
		unsigned int seq;

		// 网络设备可以使用L2帧头缓存(dev->header_ops->cache),但是还没有建立缓存(dst->hh)
		if (dev->header_ops->cache && !neigh->hh.hh_len)
			neigh_hh_init(neigh);

		do {
			__skb_pull(skb, skb_network_offset(skb));
			seq = read_seqbegin(&neigh->ha_lock);
			//设置数据包的目标 MAC 地址
			err = dev_hard_header(skb, dev, ntohs(skb->protocol),
					      neigh->ha, NULL, skb->len);
		} while (read_seqretry(&neigh->ha_lock, seq));
		
		// 首部构造成功,输出数据包
		if (err >= 0)
			rc = dev_queue_xmit(skb);
		else
			goto out_kfree_skb;
	}
out:
	return rc;
out_kfree_skb:
	rc = -EINVAL;
	kfree_skb(skb);
	goto out;
}

6、网络设备层进行数据传输

如果上述过程一次顺利,那么就会调用到网络设备层的dev_queue_xmit,将数据做进一步处理

int dev_queue_xmit(struct sk_buff *skb)
{
	return __dev_queue_xmit(skb, NULL);
}

static int __dev_queue_xmit(struct sk_buff *skb, void *accel_priv)
{
	struct net_device *dev = skb->dev;
	//struct netdev_queue用于表示一个网络设备的发送队列
	struct netdev_queue *txq;
	//struct Qdisc负责决定如何排队和发送数据包
	struct Qdisc *q;
	int rc = -ENOMEM;

	//设置mac的头部偏移
	skb_reset_mac_header(skb);

	if (unlikely(skb_shinfo(skb)->tx_flags & SKBTX_SCHED_TSTAMP))
		__skb_tstamp_tx(skb, NULL, skb->sk, SCM_TSTAMP_SCHED);

	rcu_read_lock_bh();

	skb_update_prio(skb);

	qdisc_pkt_len_init(skb);
#ifdef CONFIG_NET_CLS_ACT
	skb->tc_verd = SET_TC_AT(skb->tc_verd, AT_EGRESS);
# ifdef CONFIG_NET_EGRESS
	if (static_key_false(&egress_needed)) {
		skb = sch_handle_egress(skb, &rc, dev);
		if (!skb)
			goto out;
	}
# endif
#endif
	//现在已经处于mac层,不需要对应的路由信息,因此可以将路由信息删除
	if (dev->priv_flags & IFF_XMIT_DST_RELEASE)
		skb_dst_drop(skb);
	else
		skb_dst_force(skb);

	//获取网络设备的队列
	txq = netdev_pick_tx(dev, skb, accel_priv);
	//从netdev_queue结构上取下设备的qdisc 
	q = rcu_dereference_bh(txq->qdisc);

	trace_net_dev_queue(skb);
	/*如果Qdisc有对应的enqueue规则,就会调用__dev_xmit_skb,进入带有拥塞的控制的Flow,注意这个地方,虽然是走拥塞控制的
	 *Flow但是并不一定非得进行enqueue操作,只有Busy的状况下,才会走Qdisc的enqueue/dequeue操作进行
	 */
	if (q->enqueue) {
		rc = __dev_xmit_skb(skb, q, dev, txq);
		goto out;
	}
	//此处是设备没有Qdisc的,实际上没有enqueue/dequeue的规则,无法进行拥塞控制的操作,则直接发送
	if (dev->flags & IFF_UP) {
		// 当前CPU编号
		int cpu = smp_processor_id(); /* ok because BHs are off */

		if (txq->xmit_lock_owner != cpu) {
			if (unlikely(__this_cpu_read(xmit_recursion) >
				     XMIT_RECURSION_LIMIT))
				goto recursion_alert;

			skb = validate_xmit_skb(skb, dev);
			if (!skb)
				goto out;

			HARD_TX_LOCK(dev, txq, cpu);
			//这个地方判断一下txq不是stop状态,那么就直接调用dev_hard_start_xmit函数来发送数据
			if (!netif_xmit_stopped(txq)) {
				__this_cpu_inc(xmit_recursion);
				skb = dev_hard_start_xmit(skb, dev, txq, &rc);
				__this_cpu_dec(xmit_recursion);
				// 如果发送完成,就解锁
				if (dev_xmit_complete(rc)) {
					HARD_TX_UNLOCK(dev, txq);
					goto out;
				}
			}
			HARD_TX_UNLOCK(dev, txq);
			net_crit_ratelimited("Virtual device %s asks to queue packet!\n",
					     dev->name);
		} else {
recursion_alert:
			net_crit_ratelimited("Dead loop on virtual device %s, fix it urgently!\n",
					     dev->name);
		}
	}
	rc = -ENETDOWN;
	rcu_read_unlock_bh();

	atomic_long_inc(&dev->tx_dropped);
	kfree_skb_list(skb);
	return rc;
out:
	rcu_read_unlock_bh();
	return rc;
}

先检查是否有enqueue的规则,如果有即调用__dev_xmit_skb进入拥塞控制的flow,如果没有且txq处于On的状态,那么就调用dev_hard_start_xmit直接发送到driver,好 那先分析带Qdisc策略的flow 进入__dev_xmit_skb

static inline int __dev_xmit_skb(struct sk_buff *skb, struct Qdisc *q,
				 struct net_device *dev,
				 struct netdev_queue *txq)
{
	spinlock_t *root_lock = qdisc_lock(q);
	struct sk_buff *to_free = NULL;
	bool contended;
	int rc;

	qdisc_calculate_pkt_len(skb, q);
	/*
	 * Heuristic to force contended enqueues to serialize on a
	 * separate lock before trying to get qdisc main lock.
	 * This permits qdisc->running owner to get the lock more
	 * often and dequeue packets faster.
	 */
	contended = qdisc_is_running(q);
	if (unlikely(contended))
		spin_lock(&q->busylock);

	spin_lock(root_lock);
	//主要是判定Qdisc的state: __QDISC_STATE_DEACTIVATED,如果处于非活动的状态,就DROP这个包,返回NET_XMIT_DROP
	if (unlikely(test_bit(__QDISC_STATE_DEACTIVATED, &q->state))) {
		__qdisc_drop(skb, &to_free);
		rc = NET_XMIT_DROP;
		//(q->flags & TCQ_F_CAN_BYPASS)表示qdisc允许数据包绕过排队系统
		//!qdisc_qlen(q)表示qdisc的队列中没有等待发送的数据
		//qdisc_run_begin(q)判断队列是否是运行状态,如果是,返回true,否则将状态设置为运行状态,然后返回false
		//网络没有拥塞
	} else if ((q->flags & TCQ_F_CAN_BYPASS) && !qdisc_qlen(q) &&
		   qdisc_run_begin(q)) {
		/*
		 * This is a work-conserving queue; there are no old skbs
		 * waiting to be sent out; and the qdisc is not running -
		 * xmit the skb directly.
		 */
		//增加qdisc发送的字节数和数据包数
		qdisc_bstats_update(q, skb);

		//发送数据包
		if (sch_direct_xmit(skb, q, dev, txq, root_lock, true)) {
			if (unlikely(contended)) {
				spin_unlock(&q->busylock);
				contended = false;
			}
			//如果发送数据包成功,继续发生
			__qdisc_run(q);
			//如果发送数据包失败,那么会调用qdisc_run_end将队列的状态设置为停止状态
		} else
			qdisc_run_end(q);

		rc = NET_XMIT_SUCCESS;
		//网络拥塞
	} else {
		//将数据包加入队列
		rc = q->enqueue(skb, q, &to_free) & NET_XMIT_MASK;
		//如果Qdisc q不是运行状态,就设置成运行状态
		if (qdisc_run_begin(q)) {
			if (unlikely(contended)) {
				spin_unlock(&q->busylock);
				contended = false;
			}
			__qdisc_run(q);
		}
	}
	spin_unlock(root_lock);
	if (unlikely(to_free))
		kfree_skb_list(to_free);
	if (unlikely(contended))
		spin_unlock(&q->busylock);
	return rc;
}

如果发送了网络拥塞,则会将数据放到设备的发送队列中,如果没有发生网络拥塞,那么就会调用sch_direct_xmit函数发生数据

下面来分析sch_direct_xmit,这个函数可能传输几个数据包,因为在不经过queue状况下和经过queue的状况下都会调通过这个函数发送,如果是queue状况,肯定是能够传输多个数据包了

int sch_direct_xmit(struct sk_buff *skb, struct Qdisc *q,
		    struct net_device *dev, struct netdev_queue *txq,
		    spinlock_t *root_lock, bool validate)
{
	int ret = NETDEV_TX_BUSY;

	/* And release qdisc */
	//调用该函数时队列策略的队列锁已经被锁了,现在解锁
	spin_unlock(root_lock);

	if (validate)
		skb = validate_xmit_skb_list(skb, dev);
	//如果这个skb有效
	if (likely(skb)) {
		//取得发送队列的锁
		HARD_TX_LOCK(dev, txq, smp_processor_id());
		//如果发送队列已经开启
		if (!netif_xmit_frozen_or_stopped(txq))
			/*如果说txq被stop,即置位QUEUE_STATE_ANY_XOFF_OR_FROZEN,就直接ret = NETDEV_TX_BUSY
		 	*如果说txq 正常运行,那么直接调用dev_hard_start_xmit发送数据包*/
			skb = dev_hard_start_xmit(skb, dev, txq, &ret);

		HARD_TX_UNLOCK(dev, txq);
	} else {
		spin_lock(root_lock);
		return qdisc_qlen(q);
	}
	spin_lock(root_lock);
	//进行返回值处理! 如果ret < NET_XMIT_MASK 为true 否则 flase
	if (dev_xmit_complete(ret)) {
		//这个地方需要注意可能有driver的负数的case,也意味着这个skb被drop了
		ret = qdisc_qlen(q);
	} else {
		if (unlikely(ret != NETDEV_TX_BUSY))
			net_warn_ratelimited("BUG %s code %d qlen %d\n",
					     dev->name, ret, q->q.qlen);
		//发生Tx Busy的时候,重新进行requeu
		ret = dev_requeue_skb(skb, q);
	}

	//如果txq stop并且ret !=0  说明已经无法发送数据包了ret = 0
	if (ret && netif_xmit_frozen_or_stopped(txq))
		ret = 0;

	return ret;
}

如果前面调用dev_hard_start_xmit发送数据失败,则会调用dev_requeue_skb把skb放到发送队列中,并设置对应的软中断,当网卡不忙时,就会触发,然后再次发送数据

继续看dev_hard_start_xmit,这个函数比较简单,调用xmit_one来发送一个到多个数据包了

struct sk_buff *dev_hard_start_xmit(struct sk_buff *first, struct net_device *dev,
				    struct netdev_queue *txq, int *ret)
{
	struct sk_buff *skb = first;
	int rc = NETDEV_TX_OK;

	while (skb) {
		struct sk_buff *next = skb->next;

		skb->next = NULL;
		//将此数据包送到driver Tx函数,因为dequeue的数据也会从这里发送,所以会有netx
		rc = xmit_one(skb, dev, txq, next != NULL);
		//如果发送不成功,next还原到skb->next 退出
		if (unlikely(!dev_xmit_complete(rc))) {
			skb->next = next;
			goto out;
		}

		/*如果发送成功,把next置给skb,一般的next为空 这样就返回,如果不为空就继续发!*/
		skb = next;
		//如果txq被stop,并且skb需要发送,就产生TX Busy的问题
		if (netif_xmit_stopped(txq) && skb) {
			rc = NETDEV_TX_BUSY;
			break;
		}
	}

out:
	*ret = rc;
	return skb;
}

对于xmit_one这个来讲比较简单了,下面代码中列出了xmit_one, netdev_start_xmit,__netdev_start_xmit 这个三个函数,其目的就是将封包送到driver的tx函数了

static int xmit_one(struct sk_buff *skb, struct net_device *dev,
		    struct netdev_queue *txq, bool more)
{
	unsigned int len;
	int rc;
	/*如果有抓包的工具的话,这个地方会进行抓包,such as Tcpdump*/
	if (!list_empty(&ptype_all) || !list_empty(&dev->ptype_all))
		dev_queue_xmit_nit(skb, dev);

	len = skb->len;
	trace_net_dev_start_xmit(skb, dev);
	/*调用netdev_start_xmit,快到driver的tx函数了*/
	rc = netdev_start_xmit(skb, dev, txq, more);
	trace_net_dev_xmit(skb, rc, dev, len);

	return rc;
}

static inline netdev_tx_t netdev_start_xmit(struct sk_buff *skb, struct net_device *dev,
					    struct netdev_queue *txq, bool more)
{
	//获取对应网卡驱动的操作函数集
	const struct net_device_ops *ops = dev->netdev_ops;
	int rc;

	/*__netdev_start_xmit 里面就完全是使用driver 的ops去发包了,其实到此为止,一个skb已经从netdevice
	 *这个层面送到driver层了,接下来会等待driver的返回*/
	rc = __netdev_start_xmit(ops, skb, dev, more);
	/*如果返回NETDEV_TX_OK,那么会更新下Txq的trans时间戳哦,txq->trans_start = jiffies;*/
	if (rc == NETDEV_TX_OK)
		txq_trans_update(txq);

	return rc;
}

static inline netdev_tx_t __netdev_start_xmit(const struct net_device_ops *ops,
					      struct sk_buff *skb, struct net_device *dev,
					      bool more)
{
	skb->xmit_more = more ? 1 : 0;
	//调用网卡驱动的发送函数,真正的通过硬件发送数据
	return ops->ndo_start_xmit(skb, dev);
}

7、网卡驱动层进行数据传输

通过调用ndo_start_xmit函数,数据才能真正被网卡发送出去。对应不同的网卡驱动,ndo_start_xmit都有各自的实现方式,也是必须要实现的,这样才能和上层的协议栈衔接起来

以DM9000驱动为例

/*分析DM9000发生数据函数**/
/*
 *  Hardware start transmission.
 *  Send a packet to media from the upper layer.
 */
static int
dm9000_start_xmit(struct sk_buff *skb, struct net_device *dev)
{
	unsigned long flags;
	board_info_t *db = netdev_priv(dev);
 
	dm9000_dbg(db, 3, "%s:\n", __func__);
 
	if (db->tx_pkt_cnt > 1)
		return NETDEV_TX_BUSY;
 
	spin_lock_irqsave(&db->lock, flags);
 
	/* Move data to DM9000 TX RAM */
	//写数据到DM9000 Tx RAM中, 写地址自动增加
	writeb(DM9000_MWCMD, db->io_addr);
 
	/*将skb中的数据写入寄存器,然后发送字节改变*/
	(db->outblk)(db->io_data, skb->data, skb->len);
	dev->stats.tx_bytes += skb->len;
 
	db->tx_pkt_cnt++;
	/*第一个发送包立刻发送, 第二个排列到发送队列中去*/
	/* TX control: First packet immediately send, second packet queue */
	if (db->tx_pkt_cnt == 1) {
		dm9000_send_packet(dev, skb->ip_summed, skb->len);
	} else {
		/* Second packet */
		db->queue_pkt_len = skb->len;
		db->queue_ip_summed = skb->ip_summed;
		
		/*告诉网络协议栈,停止发送数据。*/
		netif_stop_queue(dev);
	}
 
	spin_unlock_irqrestore(&db->lock, flags);
 
	/* free this SKB */
	/*释放skb*/
	dev_kfree_skb(skb);
	
	return NETDEV_TX_OK;
}
 
 
/*当发送完成后,会触发一次发送完成的中断。 当然要去中断处理函数中*/
static irqreturn_t dm9000_interrupt(int irq, void *dev_id)
{
	struct net_device *dev = dev_id;
	board_info_t *db = netdev_priv(dev);
	int int_status;
	unsigned long flags;
	u8 reg_save;
 
	dm9000_dbg(db, 3, "entering %s\n", __func__);
 
	/* A real interrupt coming */
 
	/* holders of db->lock must always block IRQs */
	spin_lock_irqsave(&db->lock, flags); 
 
	/* Save previous register address */
	reg_save = readb(db->io_addr);		//存储以前的地址
 
	/* Disable all interrupts */   //屏蔽所有中断
	iow(db, DM9000_IMR, IMR_PAR);
 
	/* Got DM9000 interrupt status */
	int_status = ior(db, DM9000_ISR);	/* Got ISR */
	iow(db, DM9000_ISR, int_status);	/* Clear ISR status */
 
	if (netif_msg_intr(db))
		dev_dbg(db->dev, "interrupt status %02x\n", int_status);
 
	/* Received the coming packet */   //接受中断发生
	if (int_status & ISR_PRS)
		dm9000_rx(dev);
 
	/* Trnasmit Interrupt check */
	if (int_status & ISR_PTS)					//检测是否发送完成
		dm9000_tx_done(dev, db);
 
	if (db->type != TYPE_DM9000E) {
		if (int_status & ISR_LNKCHNG) {
			/* fire a link-change request */
			schedule_delayed_work(&db->phy_poll, 1);
		}
	}
 
	/* Re-enable interrupt mask */
	iow(db, DM9000_IMR, db->imr_all);		//使能中断
 
	/* Restore previous register address */
	writeb(reg_save, db->io_addr);		//恢复以前的地址
 
	spin_unlock_irqrestore(&db->lock, flags);
 
	return IRQ_HANDLED;
}
 
 
/*分析中断发送完成处理函数*/
 
static void dm9000_tx_done(struct net_device *dev, board_info_t *db)
{
	int tx_status = ior(db, DM9000_NSR);	/* Got TX status */	//得到发送的状态
 
	if (tx_status & (NSR_TX2END | NSR_TX1END)) {
		/* One packet sent complete */		//是一个包就发送完成
		db->tx_pkt_cnt--;
		dev->stats.tx_packets++;
 
		if (netif_msg_tx_done(db))
			dev_dbg(db->dev, "tx done, NSR %02x\n", tx_status);
 
		/* Queue packet check & send */		//如果超过2个,进入队列。发送
		if (db->tx_pkt_cnt > 0)
			dm9000_send_packet(dev, db->queue_ip_summed,
					   db->queue_pkt_len);
		netif_wake_queue(dev);   //唤醒发送队列
	}
}
 
 
/*
* 总结:
       1. 通知网络协议栈,停止发送队列
       2. 写skb数据到寄存器中去
       3. 释放skb资源
       4. 当发送完成后,唤醒发送队列
*/

8、数据传输的整个流程

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/490858.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

windows@浏览器主页被篡改劫持@360篡改主页@广告和弹窗设置@极速版

文章目录 360篡改浏览器主页方法1锁定浏览器主页 方法2注册表修改 360广告和弹窗360极速版 小结 360篡改浏览器主页 如果您使用360,且不想卸载它,那么当你启动360后,它可能会篡改你的浏览器(比如edge)的主页start page为360早期可能是通过修改快捷方式的target等属性,但是现在…

淘宝商品详情接口:解锁淘宝海量商品信息的秘密武器!

淘宝商品详情接口技术详解 淘宝作为中国最大的电子商务平台之一&#xff0c;其开放平台提供了丰富的API接口供开发者使用&#xff0c;以便第三方应用能够与淘宝平台无缝对接&#xff0c;实现数据交互和业务逻辑。其中&#xff0c;商品详情接口是众多API中非常重要的一项&#…

【LeetCode热题100】102. 二叉树的层序遍历(二叉树)

一.题目要求 给你二叉树的根节点 root &#xff0c;返回其节点值的 层序遍历 。 &#xff08;即逐层地&#xff0c;从左到右访问所有节点&#xff09;。 二.题目难度 中等 三.输入样例 示例 1&#xff1a; 输入&#xff1a;root [3,9,20,null,null,15,7] 输出&#xff1…

算法笔记~—位运算

目录 常见位运算&#xff1a; 1、基础位运算 2、对于一个数n。确定、修改这个数n二进制x位。 3、提取&#xff08;确定&#xff09;一个数n最右侧的1&#xff08;bit&#xff09;与干掉最右侧的1&#xff08;bit&#xff09; 4、异或运算律 5、位运算的优先级&#xff1a…

网上国网App启动鸿蒙原生应用开发,鸿蒙开发前景怎么样?

从华为宣布全面启动鸿蒙生态原生应用一来&#xff0c;各种各样的新闻就没有停过&#xff0c;如&#xff1a;阿里、京东、小红书……等大厂的加入&#xff0c;而这次他们又与一个国企大厂进行合作&#xff1a; 作为特大型国有重点骨干企业&#xff0c;国家电网承担着保障安全、经…

GAMES Webinar 288-VR/AR专题-陆峰-混合现实中的多模态自然人机交互

感知交互增强智能 研究室虚拟现实技术与系统国家重点实验室&#xff0c;北京航空航天大学计算医学研究所&#xff0c;大数据精准医疗北京市高精尖创新中心 Perception & Hybrid Interaction (PHI) for Augmented & Affective Intelligence (A2I) We are working on v…

voxelize_cuda安装教程 python+windows环境

import voxelize_cuda报错 安装步骤&#xff1a; 克隆voxelize项目 官网&#xff1a;https://github.com/YuliangXiu/neural_voxelization_layer.git git clone https://github.com/YuliangXiu/neural_voxelization_layer.git下载一些必备的解析c文件的依赖 官网&#xff1a…

ES6 基础

文章目录 1. 初识 ES62. let 声明变量3. const 声明常量4. 解构赋值 1. 初识 ES6 ECMAScript6.0(以下简称ES6)是JavaScript语言的下一代标准&#xff0c;已经在2015年6月正式发布了。它的目标&#xff0c;是使得」JavaScript语言可以用来编写复杂的大型应用程序&#xff0c;成为…

蓝牙HFP协议推荐的语音丢包补偿算法浮点实现的定点化

最近在做蓝牙的宽带语音通话。相对于蓝牙窄带语音&#xff0c;主要变化是把采样率从8k变到16k&#xff0c;以及编解码器从CVSD变成mSBC&#xff08;modified SBC&#xff0c;改进的SBC&#xff09;等。蓝牙语音通话相关的HFP&#xff08;Hand Free Profile&#xff09;强烈建议…

【线段树】第十三届蓝桥杯省赛C++ A组 Java C组 Python A组/B组《最长不下降子序列》(C++)

【题目描述】 给定一个长度为 N 的整数序列&#xff1a;,,⋅⋅⋅,。 现在你有一次机会&#xff0c;将其中连续的 K 个数修改成任意一个相同值。 请你计算如何修改可以使修改后的数列的最长不下降子序列最长&#xff0c;请输出这个最长的长度。 最长不下降子序列是指序列中的…

报道 | 2024年4月-2024年6月国际运筹优化会议汇总

封面图来源&#xff1a; https://www.pexels.com/zh-cn/photo/1181406/ 2023年2月-2024年6月召开会议汇总&#xff1a; The 24th European Conference on Evolutionary Computation in Combinatorial Optimisation (EvoCOP) Location: Aberystwyth, Wales, UK Important Date…

鸿蒙HarmonyOS应用开发——组件级配置

在开发应用时&#xff0c;需要配置应用的一些标签&#xff0c;例如应用的包名、图标等标识特征的属性。本文描述了在开发应用需要配置的一些关键标签。 应用包名配置 应用需要在工程的AppScope目录下的 app.json5配置文件 中配置bundleName标签&#xff0c;该标签用于标识应用…

STM32F4x7标准库带操作系统移植LWIP

上一篇解读了使用STM的标准库&#xff0c;移植不带操作系统版本的LWIP。 这里再梳理一下&#xff0c;带操作系统版本的差异。 main()函数 初始化部分跟之前的基本相同。 不同的是&#xff0c;不需要在主循环里调用LwIP_Periodic_Handle(LocalTime); LWIP驱动 ethernetif.c要…

React项目打包优化-包体积分析

1、什么是包体积分析&#xff1f; 通过可视化的方式&#xff0c;直观的看到各种包打包之后的体积大小&#xff0c;方便后续针对体积情况做优化 2、怎么分析包&#xff1f; 借助插件 source-map-explorer&#xff0c; 1、先安装插件 npm install source-map-explorer 2、在p…

代码随想录刷题day35|柠檬水找零根据身高重建队列最少的箭引爆气球

文章目录 day35学习内容一、柠檬水找零1.1、思路1.2、代码-正确写法 二、根据身高重建队列2.1、思路2.2、正确写法12.2.1、 如何理解上面这段代码2.2.2、 如何理解 que.add(p[1], p)&#xff1f;2.2.3、这段代码贪心在哪里呢&#xff1f; 三、最少的箭引爆气球3.1、思路3.2、正…

YOLOv5s处理二维牙齿数据集

一、网络结构 二、输入输出 1、输入 640x640的图像 2、输出 权重文件 测试图像 三、数据预处理 在github上下载YOLOv5的模型&#xff0c;并安装模型所需环境 pip install -U -r requirements.txt 四、训练&测试 对数据集进行训练 python train.py --img 640 --batc…

mybatis 实验报告1

文章目录 新建数据库新建项目&#xff0c;并导入jar包添加配置文件conf.xml定义实体类定义操作表user的sql的映射文件 userMapper.xml注册&#xff1a;将mapper.xml文件注册到conf.xml配置文件中一共6步&#xff0c;这个只是测试类&#xff0c;这个不算 新建数据库 命名是 随便…

4、事件修饰符、过滤器、自定义指令、生命周期

一、事件修饰符 按键别名enter 回车 delete 删除键 esc取消键 space 空格键 <script> export default {name: "KeyUp",methods:{keyUp(e){ console.log(e) }},skip(){window.location.href "http:www.xx.com"} } </script> <template>…

BUUCTF-Misc14

[WUSTCTF2020]find_me1 1.打开附件 是一个学校的校徽 2.盲文解密 发现图片属性里的备注是一串盲文 用在线盲文解密 3.得到flag

第三十一天-Flask-ORM-sqlalchemy

目录 1.什么是ORM 2.flask-sqlalchemy 1安装 2.配置 3.数据库模型设计 ​编辑 4.插入修改删除 5.查询 1.什么是ORM 2.flask-sqlalchemy 1安装 2.配置 3.数据库模型设计 4.插入修改删除 5.查询