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);elsereturn -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_datastruct socket *sock = file->private_data;//这个结构体用于封装要发送的消息。这里将 iov_iter 和 kiocb 传递给 msgstruct 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_sendmsgres = 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_nosecreturn err ?: sock_sendmsg_nosec(sock, msg);
}static inline int sock_sendmsg_nosec(struct socket *sock, struct msghdr *msg)
{//调用sock->ops->sendmsgint 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标记,则超时时间为0timeo = 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的字节数,如果成功,最终会返回给应用,初始化为0copied = 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是否是发送队列的第一个skbfirst_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尚未包含任何数据,那么需要释放该skbif (!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 oneif (!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;elsebreak;}//检测当前报文是否完全处于发送窗口内,如果是则可以发送,否则不能发送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新的skbskb = 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);elsetcp_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_wndth->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_headeriph = 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为0iph->frag_off = htons(IP_DF);elseiph->frag_off = 0;iph->ttl      = ip_select_ttl(inet, &rt->dst);//设置IP头部的TTLiph->protocol = sk->sk_protocol;//设置IP头部的协议//设置源IP和目的IPip_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_outputres = 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为空,说明处理不成功,返回0if (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//获取数据包的目的地 MTUmtu = 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_output2if (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 继续传递 skbint 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);elsegoto 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_ACTskb->tc_verd = SET_TC_AT(skb->tc_verd, AT_EGRESS);
# ifdef CONFIG_NET_EGRESSif (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);elseskb_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_DROPif (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将队列的状态设置为停止状态} elseqdisc_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 否则 flaseif (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的时候,重新进行requeuret = dev_requeue_skb(skb, q);}//如果txq stop并且ret !=0  说明已经无法发送数据包了ret = 0if (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的数据也会从这里发送,所以会有netxrc = 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、数据传输的整个流程

在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Linux 在线yum安装: PostgreSQL 15.6数据库

Linux 在线yum安装&#xff1a; PostgreSQL 15.6数据库 1、PostgreSQL数据库简介2、在线安装PostgreSQL15.63、配置 PostgreSQL的环境变量4、使用默认用户登录PostgreSQL5、配置 PostgreSQL 允许远程登录6、修改 PostgreSQL 默认端口7、创建数据库和表、远程用户zyl8、pgAdmin远…

MATLAB环境下基于离散小波变换和主成分平均的医学图像融合方法

随着计算机技术和生物影像工程的日趋成熟&#xff0c;医学图像为医疗诊断提供的信息越来越丰富。目前&#xff0c;由于医学成像的设备种类繁多&#xff0c;导致医生获得的图像信息差异较大。如何把这些信息进行整合供医生使用成为当务之急。基于此&#xff0c;医学图像融合技术…

vue3+vite配置环境变量

1、创建环境变量文件&#xff1a;首先在vue3项目根目录创建.env.development 和 .env.prodution两个文件&#xff0c;分别为开发和生产环境&#xff08;必须.env.开头&#xff0c;需要额外环境&#xff0c;配置自定义的文件名称即可&#xff09; 2、在环境变量文件分别写对应…

Android内存优化项目经验分享 兼顾效率与性能

背景 项目上线一段时间后,回顾重要页面 保证更好用户体验及生产效率&#xff0c;做了内存优化和下载导出优化&#xff0c;具体效果如最后的一节的表格所示。 下面针对拍摄流程的两个页面 预览页 导出页优化实例进行介绍&#xff1a; 一.拍摄前预览页面优化 预览效果问题 存在…

试试前端自动化测试(基础篇)

众所周知的原因&#xff0c;前端作为一种特殊的 GUI 软件&#xff0c;做自动化测试困难重重。在快速迭代&#xff0c;UI 变动大的业务中&#xff0c;自动化测试想要落地更是男上加男 &#x1f436;。 近期的学习过程中&#xff0c;翻阅了众多前端自动化测试相关的文章&#xf…

【3D reconstruction 学习笔记】

三维重建 3D reconstruction 1. 相机几何针孔相机摄像机几何 2. 相机标定线性方程组的解齐次线性方程组的解非线性方程组的最小二乘解透镜相机标定带畸变的相机标定 3. 单视图重建2D平面上的变换3D空间上的变换单视测量无穷远点 无穷远线 无穷远平面影消点 影消线单视重构 4. 三…

天艺制盖邀您参观2024第七届世界燕窝及天然滋补品博览会

2024第七届世界燕窝及天然滋补品博览会 2024年8月7-9日| 上海新国际博览中心 上海燕博会 世界燕窝及天然滋补品展览会暨世界滋补产业生态发展大会&#xff08;简称上海燕博会&#xff09;&#xff0c;2017年创办于中国上海&#xff0c;是一年一度的世界燕窝滋补品行业盛会。…

运放PSRR与开关电源纹波分析的实际案例分享!

本文来自看海原创视频教程&#xff1a;《运放秘籍》运算放大器基础精讲及应用第一部*开天 微信公众号&#xff1a;工程师看海 【淘宝】https://m.tb.cn/h.5PAjLi7?tkvmMLW43KO7q CZ3457 「运放秘籍_运算放大器Multisim仿真视频教程第一部开天_工程师看海」 点击链接直接打开 …

k8s入门到实战(一)—— kubernetes概述

k8s 概述 k8s github地址&#xff1a;https://github.com/kubernetes/kubernetes 官方文档&#xff1a;https://kubernetes.io/zh-cn/docs/home/ k8s&#xff0c;全程是 kubernetes&#xff0c;这个名字源于希腊语&#xff0c;意为"舵手"或"飞行员” k8s 这…

启动性能优化

一、应用启动慢的原因 1.在主线程执行了太多耗时的操作&#xff0c;比如加载数据&#xff0c;或者初始化三方库等等&#xff0c;导致在Application的oncreate或者Activity的oncreate方法中耗时太久 2.布局嵌套太深&#xff0c;或者一些不会立即使用的布局也在一开始一起加载到…

Offlian RL: Weighted Policy Constraints for Offline Reinforcement Learning

AAAI 2023 paper Intro 分布偏移导致离线RL对于OOD数据存在过估计问题。因此一些方法限制策略靠近行为策略。但是着很大程度受限于数据集的质量。若是数据集存在非专家&#xff0c;一个自然的问题是是否有可能构建一个更合理的策略约束方法&#xff0c;该方法通过识别数据集中…

SpringBoot 3整合Elasticsearch 8

这里写自定义目录标题 版本说明spring boot POM依赖application.yml配置新建模型映射Repository简单测试完整项目文件目录结构windows下elasticsearch安装配置 版本说明 官网说明 本文使用最新的版本 springboot: 3.2.3 spring-data elasticsearch: 5.2.3 elasticsearch: 8.1…

2024年软件测试岗现状?“我“进阶了测试开发,一路狂飙...

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 1、测试工程师的现…

SM2098EHD:巴西双电压球泡灯应用

巴西使用的双电压系统主要包括127V和240V。这种双电压系统是为了适应不同的电器设备和需求。在巴西&#xff0c;有些电器设备可能需要127V的电压&#xff0c;而有些则可能需要240V的电压。 因此&#xff0c;如果你在巴西使用电器设备&#xff0c;需要确保设备能够适应巴西的双…

【数据结构】绪论

文章目录 开篇&#xff1a;数据结构的学习之路1、开篇_数据结构在学什么2、数据结构的基本概念数据数据元素、数据项数据结构、数据对象数据类型、抽象数据类型数据结构的三要素逻辑结构数据的物理结构&#xff08;存储结构&#xff09;数据的运算 最后总结内容 3、算法的基本概…

Linux 理解文件系统、磁盘结构、软硬链接

目录 一、理解磁盘结构 1、磁盘的物理结构 2、硬件层面理解 3、磁盘的具体物理存储结构 4、进行逻辑抽象 5、磁盘文件的管理 6、创建新文件的过程 二、理解文件系统 1、文件的构成 2、为何选择4KB而非512字节作为基本单位? 3、文件系统的组成 数据块&#xff08;Data Blocks&a…

图像抠图DIS——自然图像中高精度二分图像抠图的方法(C++/python模型推理)

概述 DIS&#xff08;Dichotomous Image Segmentation&#xff09;是一种新的图像分割任务&#xff0c;旨在从自然图像中分割出高精度的物体。与传统的图像分割任务相比&#xff0c;DIS更侧重于具有单个或几个目标的图像&#xff0c;因此可以提供更丰富准确的细节。 为了研究…

Ubuntu20.04上,VTK9.3在QT5上的环境配置与开发测试

Ubuntu20.04上&#xff0c;VTK9.3在QT5上的环境配置与开发测试 1 背景介绍2 VTK9.3的编译安装2.1 安装ccmake 和 VTK 的依赖项&#xff1a;2.2 建立VTK编译文件夹并下载2.3 cmake配置VTK9.3的编译环境2.4 make编译安装VTK9.32.5 测试VTK安装是否成功 3 基于qmake的QT5的VTK9.3开…

异地共享文件如何设置?

在当今数字化时代&#xff0c;异地办公已成为常态&#xff0c;越来越多的企业和个人需要在不同地区间进行文件共享与访问。为了解决复杂网络环境下的远程连接问题&#xff0c;北京金万维科技有限公司推出了一款名为【天联】的异地组网内网穿透产品。 【天联】组网是一款由北京金…

python基础——语句

一、条件语句 就是 if else 语句 &#xff01; 代表不等于 代表等于if 关键字&#xff0c;判断语句&#xff0c;有“如果”的意思&#xff0c;后面跟上判断语句else 常和“if” 连用&#xff0c;有“否则”的意思&#xff0c;后面直接跟上冒号 …