分类
未分类

字节跳动系统技术与工程团队招聘

Linux内核高级研发工程师

工作地点:北京/上海/杭州
投递地址:https://job.toutiao.com/job/detail/50971
职位描述:
1. 针对业务需求定制Linux内核,结合业务需求开发内核新功能;
2. 结合服务特点对服务器底层/Linux内核进行性能调优;
3. 负责操作系统/内核前沿新技术的研究和应用;
职位要求:
1. 精通Linux内核,至少阅读过2-3个主要模块(调度,文件系统,网络,I/O,内存管理等)的源代码;
2. 熟悉Linux平台上的C语言编程,熟悉多进程多线程编程,熟悉socket编程;
3. 具有复杂系统软件的设计、开发和调优能力;
4. 有丰富内核故障调试或内核社区补丁提交经验者优先;
5. 有多平台(x86/ARM/RISC-V)内核与系统研发经验者优先
6. 了解主流虚拟化技术(Xen/KVM等)的实现,阅读过相关源代码者优先
7. 良好的团队合作精神,较强的沟通能力
8. 优秀的分析问题和解决问题的能力,对解决具有挑战性 问题充满激情

Linux内核研发工程师

工作地点: 北京/上海/杭州
投递地址:https://job.toutiao.com/job/detail/50977
职位描述:
1. 针对业务需求定制Linux内核,结合业务需求开发内核新功能;
2. 结合服务特点对服务器底层/Linux内核进行性能调优;
3. 负责改进,维护公司内部内核,保障内核稳定且高效。
职位要求:
1. 精通Linux内核,至少阅读过1个主要模块(调度,文件系统,网络,I/O,内存管理等)的源代码;
2. 熟悉Linux平台上的C语言编程,熟悉多进程多线程编程,熟悉socket编程;
3. 善于学习新的知识,动手能力强,有进取心;
4. 有内核社区补丁提交经验者优先;
5. 有丰富内核故障调试经验者优先。

网络高级研发工程师

工作地点:北京/上海/杭州
投递地址:https://job.toutiao.com/job/detail/50988
职位描述:
1. 结合业务对Linux内核协议栈/用户态协议栈/RDMA等进行优化;
2. 负责公司内部相关 SDN、NFV 的设计、优化和开发;
3. 参与高性能网络/网络虚拟化研发;
4. 参与分布式网关/ACL/网络QoS 研发。
职位要求:
1. 3~5年相关工作经验,熟悉Linux操作系统下开发,熟悉Linux操作系统工作机制,熟悉Linux常见网络模型;
2. 熟悉Linux操作系统下C/C++开发, 能运用常见工具定位调试问题代码;
3. 熟悉了解 P4 语言,了解 openflow 协议,有 barefoot P4 相关开发经验优先;
4. 熟悉Linux kernel 或 FreeBSD 相关网络协议栈优先;
5. 熟悉路由器/交换机工作原理,熟悉NAT、VPN、负载均衡、SDN、NFV、overlay等网络技术
6. 熟悉 openstack neutron、openvswitch 等开源技术优先。

网络研发工程师

工作地点:北京/上海/杭州
投递地址:https://job.toutiao.com/job/detail/50991
职位描述:
1. 结合业务对Linux内核协议栈等进行优化;
2. 参与高性能网络/网络虚拟化研发;
3. 参与分布式网关/ACL 研发。
职位要求:
1. 熟悉Linux操作系统下开发,熟悉Linux操作系统工作机制,熟悉Linux常见网络模型;
2. 熟悉Linux操作系统下C/C++开发, 能运用常见工具定位调试问题代码;
3. 熟悉Linux kernel 或 FreeBSD 相关网络协议栈优先;
4. 熟悉路由器/交换机工作原理,熟悉NAT、VPN等网络技术。

虚拟化高级研发工程师

工作地点:北京/上海/杭州
投递地址:https://job.toutiao.com/job/detail/51032
职位描述:
1. 负责优化虚拟化技术的性能和稳定性;
2. 负责系统通用虚拟化技术前沿探索,不断满足业务需求;
3. 负责研究轻量虚拟化/安全容器等serverless场景下单机系统技术,满足业务需求。
岗位要求:
1. 精通C/C++编程和多线程性能优化,对系统编程有深入的理解;
2. 熟悉x86体系架构,深入理解操作系统原理及Linux内核,熟悉内核开发;
3. 有KVM/Xen,QEMU,Libvirt相关开发经验者优先;
4. 对Kata Containers、firecracker、crosvm、rust-vmm等有深入研究者优先;
5. 有较强硬件虚拟化研发经验者优先。

虚拟化研发工程师

工作地点:北京/上海/杭州
投递地址:https://job.toutiao.com/job/detail/51039
职位描述:
1. 负责优化虚拟化技术的性能和稳定性;
2. 开发虚拟化性能分析工具和监控工具;
3. 参与系统通用虚拟化技术前沿探索,不断满足业务需求。
岗位要求:
1. 精通C/C++编程和多线程性能优化,对系统编程有深入的理解;
2. 熟悉x86体系架构,深入理解操作系统原理及Linux内核;
3. 有KVM/Xen,QEMU,Libvirt相关开发经验者优先;
4. 对Kata Containers、firecracker、crosvm、rust-vmm等有深入研究者优先;
5. 有较强硬件虚拟化研发经验者优先。

系统软件工程师

工作地点:北京
投递地址:https://job.toutiao.com/job/detail/51040
职位描述:
1. 优化系统级基础软件设施,包括操作系统及其组件、基础库性能等;
2. 改进和优化系统监控、软件布署及其升级的自动化运维实现;
3. 优化巨量服务器的自动化管理及其维护方式;
4. 基本的系统软硬件性能瓶颈分析、故障trouble shooting排查。
职位要求:
1. 熟悉操作系统其及组件,对Linux内核有基本的了解和认识;
2. 熟悉C或者C++,并掌握其它语言如Python/Golang/Rust/Java的至少一种;
3. 熟悉常用的数据结构和算法、熟悉多线程编程及多线程程序性能优化;
4. 能够团队合作完成中大型系统软件的设计实现,代码健壮性好,有初步的代码性能优化经验;
5. 至少熟悉网络、存储、内存管理、进程调度、或者服务器硬件架构的其中一项。

系统数据平台研发工程师

工作地点:北京
投递地址:https://job.toutiao.com/job/detail/51041
职位描述:
1. 负责超大规模数据中心系统和网络监控数据平台的设计和研发;
2. 设计和构建系统网络和监控大数据分析技术架构;
3. 应用机器学习技术提升系统和网络核心监控能力。
职位要求:https://job.toutiao.com/job/detail/51094
1. 熟悉操作系统原理,有良好的数据结构和算法基础;
2. 熟悉至少一个数据处理平台,如 Map Reduce、Flink、Spark, 熟悉至少一种大数据数据库如;TSDB, Druid, ClickHouse,InfluxDB。PB级或以上数据处理经验优先;
3. 熟练掌握至少 Python/Go/Java 等编程语言之一,具备优秀的编码能力;
4. 自我驱动,具备良好的沟通技能,对新技术有强烈的学习热情;
5. 有统计或机器学习(TensorFlow/pytorch/MxNet)工作背景者优先;
6. 有相关开源社区工作经验者优先。

SRE工程师-系统技术方向

工作地点:北京
投递地址:
岗位职责:
1. 推进系统技术现有平台化服务的迭代和优化(系统压测平台、发布平台、系统滚动平台等服务/平台);
2. 推进新系统技术服务化、平台化、自动化研发落地;
3. 对海量数据进行数据清洗和整合,并对相关数据进行分析,构建健康预测能力和监控指标;
4. 构建底层技术的 CI/CD 机制,保证系统稳定性。
岗位要求:
1. 掌握常用开发语言 C/C++/Python/Golang/Shell;
2. 熟悉 Linux 操作系统,熟悉各种网络协议,对 linux kernel 有了解或者对底层技术有浓厚兴趣;
3. 有较强的学习能力,能够熟练阅读涉及产品和技术的英文文档;
4. 能够独立完成工作,具有较强的综合分析问题及解决问题的能力;
5. 有良好的工作文档习惯,及时按要求撰写更新工作流程及技术文档;
6. 有 AIOPS 项目经验者优先;有开源项目贡献者或开源项目领导者优先;

编译器高级研发工程师

工作地点:北京
投递地址:https://job.toutiao.com/job/detail/51042
工作职责:
1. 负责开发与维护公司编译器工具链;
2. 负责对大型工程的代码生成以及编译器优化分析调优;
3. 深入研究编译器,优化落地并贡献开源社区。
任职要求:
1. 熟悉编译器架构与算法,有设计与实现编译各阶段的经验,包括语言处理,编译器优化和代码生成等;
2. 较强的C/C++开发能力,有丰富的问题分析定位与调试经验;
3. 熟悉业界主流的编译器的设计与开发,如LLVM,GCC;
4. 对编译器中间语言表达机制有深入的理解,有相关设计与优化经验;熟练阅读,解析,修改,优化IR指令;
5. 熟悉ARM汇编优先;对Buck、Bazel、Blade Build等编译构建系统有深入研究或者深度使用者优先。

编译器研发工程师

工作地点:北京
投递地址:https://job.toutiao.com/job/detail/51043
工作职责:
1. 参与开发与维护公司编译器工具链;
2. 针对大型工程的代码生成以及编译器优化分析调优;
3. 深入研究编译器新技术,优化落地并贡献社区。
任职要求:
1. 熟练使用Linux;熟练使用git;熟练使用make/cmake等开发工具;
2. 较强的C/C++开发能力,有丰富的问题分析定位与调试经验;
3. 熟悉数据结构及算法;
4. 深入掌握基本的编译原理知识;
5. 熟悉业界至少1个主流的编译器的设计与开发,如LLVM/GCC等。

服务器固件研发工程师

工作地点:北京/上海/杭州
投递地址:https://job.toutiao.com/job/detail/51044
职位描述:
1. 负责海量服务器硬件组件的各固件统一化定制(与OEM、ODM协同研发);
2. 负责下一代服务器、及自研硬件的固件软件开发及上线。
职位要求:
1. 两年以上相关工作经验,有熟练阅读英文技术规范、硬件手册的能力;
2. 熟悉C或者C++开发、具有良好的数据结构算法能力;
3. 符合以下要求其中两条或以上:
4. 熟悉Intel微处理器架构、或者Intel PCH芯片架构(或者ARM服务器对应的技术),并有相关固件代码经验;
5. 熟悉Legacy BIOS、UEFI、EDK II至少其一,熟悉ACPI标准,并有相关组件的代码开发经验;
6. 熟悉商用BMC或者OpenBMC的实现,并在此方向上有丰富的嵌入式C/C++编程经验;
7. 熟悉硬件故障处理的OS与固件的交互逻辑,并有相关代码开发经验;
8. 有TPU、GPU、高速网卡、SSL / Zip加速器其中之一的固件开发经验。

分类
未分类

trace_event 注册以及使能

上一篇文章我们看到,内核展开一个 trace_point的宏以后,定义了一个trace_event_call 结构,并把这个结构放在”_ftrace_events” 段中。这一篇文章我们将解析 内核是如何加载 这些结构并使能的.
如果是内核vmlinux中的trace将在这个函数中 直接初始化,如果是模块中的调用trace_module_add_events 初始化。 这里只节选一个函数。

 static __init int event_trace_enable(void)
 {
 >-------struct trace_array *tr = top_trace_array();
 >-------struct trace_event_call **iter, *call;
 >-------int ret;

 >-------if (!tr)
 >------->-------return -ENODEV;

 >-------for_each_event(iter, __start_ftrace_events, __stop_ftrace_events) {

 >------->-------call = *iter;
 >------->-------ret = event_init(call);  \\关键函数,对event进行初始化,
 >------->-------if (!ret)
 >------->------->-------list_add(&call->list, &ftrace_events);
 >-------}

 >-------/*
 >------- * We need the top trace array to have a working set of trace
 >------- * points at early init, before the debug files and directories
 >------- * are created. Create the file entries now, and attach them
 >------- * to the actual file dentries later.
 >------- */
 >-------__trace_early_add_events(tr);

 >-------early_enable_events(tr, false);

 >-------trace_printk_start_comm();

 >-------register_event_cmds();

 >-------register_trigger_cmds();

 >-------return 0;
 }

event_init 函数如下,

 static int event_init(struct trace_event_call *call)
 {
 >-------int ret = 0;
 >-------const char *name;

 >-------name = trace_event_name(call);
 >-------if (WARN_ON(!name))
 >------->-------return -EINVAL;

 >-------if (call->class->raw_init) {
 >------->-------ret = call->class->raw_init(call);         \\ trace_event_raw_init函数
 >------->-------if (ret < 0 && ret != -ENOSYS)
 >------->------->-------pr_warn("Could not initialize trace events/%s\n", name);
 >-------}

 >-------return ret;
 }

这个函数只是调用trace_event_raw_init函数,这个函数调用了 register_trace_event 函数。这个register_trace_event 函数只是将trace_event 注册到全局结构中。

注册完成以后 会调用 __add_event_to_tracers 将trace_event 加入到各个tracer中,并在 /sys/kernel/debug/tracing/events/ 对应的目录下创建控制文件。到这一步 注册就完成了。

使能操作,使能时时给enable 文件中输入1
调用链条如下

system_enable_write --> __ftrace_set_clr_event --> __ftrace_set_clr_event_nolock -->  ftrace_event_enable_disable --> __ftrace_event_enable_disable -->trace_event_reg --> tracepoint_probe_register --> tracepoint_probe_register_prio -->tracepoint_add_func

当这一串函数执行玩意后,在trace_出会执行 trace_event_rawevent##call 对应的函数。 到这儿使能完毕。

raw_trace_point 简介
基于 bpf的raw trace_point 逻辑非常简单, 宏展开如下

 #define DECLARE_EVENT_CLASS(call, proto, args, tstruct, assign, print)>-\
 static notrace void>---->------->------->------->------->------->-------\
 __bpf_trace_##call(void *__data, proto)>>------->------->------->-------\
 {>------>------->------->------->------->------->------->------->-------\
 >-------struct bpf_prog *prog = __data;>>------->------->------->-------\
 >-------CONCATENATE(bpf_trace_run, COUNT_ARGS(args))(prog, CAST_TO_U64(args));>-\
 }

 /*
  * This part is compiled out, it is only here as a build time check
  * to make sure that if the tracepoint handling changes, the
  * bpf probe will fail to compile unless it too is updated.
  */
 #undef DEFINE_EVENT
 #define DEFINE_EVENT(template, call, proto, args)>------>------->-------\
 static inline void bpf_test_probe_##call(void)>->------->------->-------\
 {>------>------->------->------->------->------->------->------->-------\
 >-------check_trace_callback_type_##call(__bpf_trace_##template);>------\
 }>------>------->------->------->------->------->------->------->-------\
 static struct bpf_raw_event_map>__used>->------->------->------->-------\
 >-------__attribute__((section("__bpf_raw_tp_map")))>--->------->-------\
 __bpf_trace_tp_map_##call = {>-->------->------->------->------->-------\
 >-------.tp>---->-------= &__tracepoint_##call,>>------->------->-------\
 >-------.bpf_func>------= (void *)__bpf_trace_##template,>------>-------\
 >-------.num_args>------= COUNT_ARGS(args),>---->------->------->-------\
 };


 #undef DEFINE_EVENT_PRINT
 #define DEFINE_EVENT_PRINT(template, name, proto, args, print)>-\
 >-------DEFINE_EVENT(template, name, PARAMS(proto), PARAMS(args))

在4.19内核里面 还不支持 module 添加raw_tracepoint .通过bpf 可以在raw trace_point中添加任何执行,输入参数是trace_point直接输入。 有兴趣的可以看看bcc 是如何用raw_tracepoint 抓 一些数据的

注意事项:
trace_event 在复值并把 数据放到buffer的时候并没有格式化成字符串,只有当调用 读trace_pipe 或则trace文件时才会正真的将变量格式化成字符串输入。因此如果在写trace_point的时候 赋值指针的。去读的时候可能得到的不是当时的值。

分类
未分类

SACK 分析

首先我们要看 sack 和dsack的 rfc 链接如下 sack https://tools.ietf.org/html/rfc2018 dsack https://tools.ietf.org/html/rfc2883 要理解 kernel 中是怎么实现 sack和dsack。需要知道以下几个关键点

  1. kernel 发送数据包是以 skb为基础单元。发送完成以后会议skb的seq number作为key ,将这些发送完成的skb 插入到红黑树中,数据结构是(sk->tcp_rtx_queue)

  2. 当发送端接收到ack后,会遍历 sk->tcp_rtx_queue 中的skb。如果这个skb整个都在ack的seq范围内,则会删除释放这个skb。 否则 只释放skb前部分内存,调整skb的seq。

  3. 当接收到sack 时,内核会做会根据sack的分段,查找红黑树,sack最多可以包含4个分段,每个分段分别包含start_seq 和 end_seq。

    • 内核会首先使用start_seq在红黑树中找到包含 这个seq的skb即(skb start_seq < start_seq < skb end_seq)

    • 找到这个skb后,内核会将这个skb,列分为两个skb,将两个skb都插入红黑树中,第一部分不标记TCPCB_SACKED_CACKED.第二部分标记为TCPCB_SACKED_CACKED,标记在TCP_SKB_CB的sacked结构中。

    • 内核继续沿着红黑树查找下一个skb.如果这个skb的 end_seq 小于 sack的end_seq, 则会将这个skb 与上一个skb 合并。

    • 直到找到一个skb 他的 end_seq 大于 sack的end_seq。这时候,内核会将这个skb 以sack中的end_seq为分析界限,分成两部分。 第一部分的数据合并到上一个skb。后面的数据继续保持独立。

  4. 当重传时,会从这个红黑树中找skb。如果skb的sacked 已经标记为 TCPCB_SACKED_ACKED|TCPCB_SACKED_RETRANS 则不会重传,否则重传。

  5. 虽然红黑树中是以skb为单元记录。但是列分时是以mss 为最小单元的,即一个skb 的长度是mss的倍数,列分时,只能列分出去mss的倍数的长度。skb中包含多少个mss的长度记录在TCP_SKB_CB数据结构中的tcp_gso_segs成员中,注意这个成员是16位的。

在理解以上几点的基础上我们看代码。 首先看数据包发送的代码,在tcp_event_new_data_sent 函数中将发送出去的skb 插入红黑树。在tso_fragment函数中会计算tcp_gso_segs的值。

tcp_sendmsg 
    --&gt; tcp_sendmsg_locked 
        --&gt;tcp_push
            --&gt;__tcp_push_pending_frames 
                --&gt;tcp_write_xmit 
                    --&gt;tso_fragment
                    --&gt;tcp_event_new_data_sent

在看快重传逻辑。在tcp_xmit_retransmit_queue函数中对过滤掉已经有 TCPCB_SACKED_ACKED标记的skb。

tcp_ack
    --&gt;tcp_fastretrans_alert
        --&gt;tcp_simple_retransmit
            --&gt;tcp_xmit_retransmit_queue
                --&gt;tcp_retransmit_skb

最后,我们看 sack 包的处理逻辑。 tcp_clean_rtx_queue 以后的函数调用是收到ack以后清理 红黑树上的skb的逻辑, tcp_clean_rtx_queue以前的逻辑是处理sack的。结合上面几点,我们可以清晰的看到 内核是怎么处理sack。

tcp_v4_rcv
    --&gt;tcp_v4_fill_cb
    --&gt;tcp_v4_do_rcv
        tcp_rcv_established
            --&gt;tcp_validate_incoming
                --&gt;tcp_fast_parse_options
                    --&gt;tcp_parse_options
            --&gt;tcp_ack
                --&gt;tcp_sacktag_write_queue
                    --&gt;tcp_sacktag_skip
                    --&gt;tcp_sacktag_walk
                        --&gt;tcp_shift_skb_data
                            --&gt;tcp_shift_skb_data
                                --&gt;skb_shift
                                --&gt;tcp_shifted_skb
                                    --&gt;tcp_sacktag_one
                        --&gt;tcp_match_skb_to_sack
                            --&gt;tcp_fragment
                        --&gt;tcp_sacktag_one
                --&gt;tcp_clean_rtx_queue
                    --&gt;tcp_tso_acked
                        --&gt;tcp_trim_head
                    --&gt;tcp_rtx_queue_unlink_and_free
分类
未分类

vmbus 分析二

上一篇写到vmbus 怎么发现 vmbus上的设备,这一篇讲vmbus的网络设备怎么probe.

当有新的vmbus设备注册 进来时,vmbus 会调用注册在上面的驱动的match函数去匹配是否可以用这个驱动来驱起这个设备,如果能驱起来则会调用probe函数来处理。
在vmbus 的网络设备上会调用 netvsc_probe函数来处理。 这里面的其他操作跟普通的网卡驱动没有什么区别,有意思的是他调用rndis_filter_device_add这个函数,这个函数里面有两个地方比较有意思 第一个是调用了netvsc_device_add函数。该函数如下。

 struct netvsc_device *netvsc_device_add(struct hv_device *device,
 >------->------->------->-------const struct netvsc_device_info *device_info)
 {
 >-------int i, ret = 0;
 >-------struct netvsc_device *net_device;
 >-------struct net_device *ndev = hv_get_drvdata(device);
 >-------struct net_device_context *net_device_ctx = netdev_priv(ndev);

 >-------net_device = alloc_net_device();
 >-------if (!net_device)
 >------->-------return ERR_PTR(-ENOMEM);

 >-------for (i = 0; i < VRSS_SEND_TAB_SIZE; i++)
 >------->-------net_device_ctx->tx_table[i] = 0;

 >-------/* Because the device uses NAPI, all the interrupt batching and
 >------- * control is done via Net softirq, not the channel handling
 >------- */
 >-------set_channel_read_mode(device->channel, HV_CALL_ISR);

 >-------/* If we're reopening the device we may have multiple queues, fill the
 >------- * chn_table with the default channel to use it before subchannels are
 >------- * opened.
 >------- * Initialize the channel state before we open;
 >------- * we can be interrupted as soon as we open the channel.
 >------- */

 >-------for (i = 0; i < VRSS_CHANNEL_MAX; i++) {
 >------->-------struct netvsc_channel *nvchan = &net_device->chan_table[i];

 >------->-------nvchan->channel = device->channel;
 >------->-------nvchan->net_device = net_device;
 >------->-------u64_stats_init(&nvchan->tx_stats.syncp);
 >------->-------u64_stats_init(&nvchan->rx_stats.syncp);
 >-------}

 >-------/* Enable NAPI handler before init callbacks */
 >-------netif_napi_add(ndev, &net_device->chan_table[0].napi,
 >------->-------       netvsc_poll, NAPI_POLL_WEIGHT);

 >-------/* Open the channel */
 >-------ret = vmbus_open(device->channel, netvsc_ring_bytes,
 >------->------->------- netvsc_ring_bytes,  NULL, 0,
 >------->------->------- netvsc_channel_cb, net_device->chan_table);

 >-------if (ret != 0) {
 >------->-------netdev_err(ndev, "unable to open channel: %d\n", ret);
 >------->-------goto cleanup;
 >-------}

 >-------/* Channel is opened */
 >-------netdev_dbg(ndev, "hv_netvsc channel opened successfully\n");

 >-------napi_enable(&net_device->chan_table[0].napi);

 >-------/* Connect with the NetVsp */
 >-------ret = netvsc_connect_vsp(device, net_device, device_info);
 >-------if (ret != 0) {
 >------->-------netdev_err(ndev,
 >------->------->-------"unable to connect to NetVSP - %d\n", ret);
 >------->-------goto close;
 >-------}

 >-------/* Writing nvdev pointer unlocks netvsc_send(), make sure chn_table is
 >------- * populated.
 >------- */
 >-------rcu_assign_pointer(net_device_ctx->nvdev, net_device);

 >-------return net_device;

 close:
 >-------RCU_INIT_POINTER(net_device_ctx->nvdev, NULL);
 >-------napi_disable(&net_device->chan_table[0].napi);

 >-------/* Now, we can close the channel safely */
 >-------vmbus_close(device->channel);

 cleanup:
 >-------netif_napi_del(&net_device->chan_table[0].napi);
 >-------free_netvsc_device(&net_device->rcu);

 >-------return ERR_PTR(ret);
 }

这个函数里面调用了 vmbus_open ,vmbus_open里面调用了两个函数 vmbus_alloc_ring和__vmbus_open函数,第一个函数是用来创建接受数据的缓冲区,第二个是设置 channel的onchannel_callback函数的, 而onchannel_callback函数会在 vmbus_on_event函数中调用,或者在vmbus_channel_isr函数中调用 或者在vmbus_on_event函数中调用,vmbus_on_event是一个tasklet函数,这个tasklet 是channel->callback_event。 vmbus_on_event代码如下

 void vmbus_on_event(unsigned long data)
 {
 >-------struct vmbus_channel *channel = (void *) data;
 >-------unsigned long time_limit = jiffies + 2;

 >-------trace_vmbus_on_event(channel);

 >-------do {
 >------->-------void (*callback_fn)(void *);

 >------->-------/* A channel once created is persistent even when
 >------->------- * there is no driver handling the device. An
 >------->------- * unloading driver sets the onchannel_callback to NULL.
 >------->------- */
 >------->-------callback_fn = READ_ONCE(channel->onchannel_callback);
 >------->-------if (unlikely(callback_fn == NULL))
 >------->------->-------return;

 >------->-------(*callback_fn)(channel->channel_callback_context);

 >------->-------if (channel->callback_mode != HV_CALL_BATCHED)
 >------->------->-------return;

 >------->-------if (likely(hv_end_read(&channel->inbound) == 0))
 >------->------->-------return;

 >------->-------hv_begin_read(&channel->inbound);
 >-------} while (likely(time_before(jiffies, time_limit)));

 >-------/* The time limit (2 jiffies) has been reached */
 >-------tasklet_schedule(&channel->callback_event);
 }
 static void vmbus_channel_isr(struct vmbus_channel *channel)
 {
 >-------void (*callback_fn)(void *);

 >-------callback_fn = READ_ONCE(channel->onchannel_callback);
 >-------if (likely(callback_fn != NULL))
 >------->-------(*callback_fn)(channel->channel_callback_context);
 }

到这里我们跟上一篇的 vmbus_isr函数对上, vmbus_isr函数会调用vmbus_chan_sched 函数.vmbus_chan_sched 会根据 callback_mode的类型选择是挂起tasklet或者直接调用onchannel_callback函数,很不巧网卡刚刚好是HV_CALL_ISR类型,因此会直接调用onchannel_callback函数。调用的函数netvsc_channel_cb 。 netvsc_channel_cb函数如下。这个函数只是简单的挂起这个channel的napi函数,最后在napi中收包。

 void netvsc_channel_cb(void *context)
 {
 >-------struct netvsc_channel *nvchan = context;
 >-------struct vmbus_channel *channel = nvchan->channel;
 >-------struct hv_ring_buffer_info *rbi = &channel->inbound;

 >-------/* preload first vmpacket descriptor */
 >-------prefetch(hv_get_ring_buffer(rbi) + rbi->priv_read_index);

 >-------if (napi_schedule_prep(&nvchan->napi)) {
 >------->-------/* disable interrupts from host */
 >------->-------hv_begin_read(rbi);

 >------->-------__napi_schedule_irqoff(&nvchan->napi);
 >-------}
 }

如果网卡只有一个队列,对着vmbus的设备来说只有一个channel,到这里已经完事, 但是如果网卡有多个队列,也就是多个channel,这个时候 netvsc_sc_open函数会在下一个channel被探测到的时候被执行,主要是因为在rndis_filter_device_add函数中执行 vmbus_set_sc_create_callback函数,该函数如下,看到这个函数我们就想到上一篇说 sub channel 被探测到以后,不会在vmbus中注册但是会执行primary channel的sc_creation_callback函数。这边会执行的就是netvsc_sc_open函数。

 void vmbus_set_sc_create_callback(struct vmbus_channel *primary_channel,
 >------->------->------->-------void (*sc_cr_cb)(struct vmbus_channel *new_sc))
 {
 >-------primary_channel->sc_creation_callback = sc_cr_cb;
 }

netvsc_sc_open函数如下。

 static void netvsc_sc_open(struct vmbus_channel *new_sc)
 {
 >-------struct net_device *ndev =
 >------->-------hv_get_drvdata(new_sc->primary_channel->device_obj);
 >-------struct net_device_context *ndev_ctx = netdev_priv(ndev);
 >-------struct netvsc_device *nvscdev;
 >-------u16 chn_index = new_sc->offermsg.offer.sub_channel_index;
 >-------struct netvsc_channel *nvchan;
 >-------int ret;

 >-------/* This is safe because this callback only happens when
 >------- * new device is being setup and waiting on the channel_init_wait.
 >------- */
 >-------nvscdev = rcu_dereference_raw(ndev_ctx->nvdev);
 >-------if (!nvscdev || chn_index >= nvscdev->num_chn)
 >------->-------return;

 >-------nvchan = nvscdev->chan_table + chn_index;

 >-------/* Because the device uses NAPI, all the interrupt batching and
 >------- * control is done via Net softirq, not the channel handling
 >------- */
 >-------set_channel_read_mode(new_sc, HV_CALL_ISR);

 >-------/* Set the channel before opening.*/
 >-------nvchan->channel = new_sc;

 >-------ret = vmbus_open(new_sc, netvsc_ring_bytes,
 >------->------->------- netvsc_ring_bytes, NULL, 0,
 >------->------->------- netvsc_channel_cb, nvchan);
 >-------if (ret == 0)
 >------->-------napi_enable(&nvchan->napi);
 >-------else
 >------->-------netdev_notice(ndev, "sub channel open failed: %d\n", ret);

 >-------if (atomic_inc_return(&nvscdev->open_chn) == nvscdev->num_chn)
 >------->-------wake_up(&nvscdev->subchan_open);
 }

在netvsc_sc_open 我们看到网卡设备打开了更多的队列,当队列数量等于channel数量后,网卡的其他设置功能可用。

vmbus 这块我觉得比较有创意的地方在于,把0xf3 这个 irq当作了一个percpu的irq,然后讲 所有的channel都共享irq。减少了irq的数量。 这个没有pci那么多 枚举的过程。把 外设当作一个主体,这种思想非常有借鉴意义。

分类
未分类

vmbus 分析 一

vmbus device 发现
1.在创建vmbus时又一个hv_setup_vmbus_irq函数内容如下,注册进去的函数是 vmbus_isr函数

static void (*vmbus_handler)(void);
 __visible void __irq_entry hyperv_vector_handler(struct pt_regs *regs)
 {
 >-------struct pt_regs *old_regs = set_irq_regs(regs);

 >-------entering_irq();
 >-------inc_irq_stat(irq_hv_callback_count);
 >-------if (vmbus_handler)
 >------->-------vmbus_handler();

 >-------if (ms_hyperv.hints & HV_DEPRECATING_AEOI_RECOMMENDED)
 >------->-------ack_APIC_irq();

 >-------exiting_irq();
 >-------set_irq_regs(old_regs);
 }

 void hv_setup_vmbus_irq(void (*handler)(void))
 {
 >-------vmbus_handler = handler;
 }
 void hv_remove_vmbus_irq(void)
 {
 >-------/* We have no way to deallocate the interrupt gate */
 >-------vmbus_handler = NULL;
 }

hyperv_vector_handler函数是一个中断处理函数,这个中断处理函数处理的是0xf3中断
当有这个中断的时候 会调用vmbus_isr进行中断处理。

vmbus_isr函数中,主要做了两件事情,第一是调用vmbus_chan_sched 函数,第二是挂起 hv_cpu->msg_dpc这个tasklet。而vmbus_chan_sched 函数中会遍历当前cpu的chan_list链表,并挂起channel->callback_event tasklet。这个tasklet最后会发现是触发网络的napi操作。 先讲 hv_cpu->msg_dpc这个tasklet

 static void vmbus_isr(void)
 {
 >-------struct hv_per_cpu_context *hv_cpu
 >------->-------= this_cpu_ptr(hv_context.cpu_context);
 >-------void *page_addr = hv_cpu->synic_event_page;
 >-------struct hv_message *msg;
 >-------union hv_synic_event_flags *event;
 >-------bool handled = false;

 >-------if (unlikely(page_addr == NULL))
 >------->-------return;

 >-------event = (union hv_synic_event_flags *)page_addr +
 >------->------->------->------->------- VMBUS_MESSAGE_SINT;
 >-------/*
 >------- * Check for events before checking for messages. This is the order
 >------- * in which events and messages are checked in Windows guests on
 >------- * Hyper-V, and the Windows team suggested we do the same.
 >------- */

 >-------if ((vmbus_proto_version == VERSION_WS2008) ||
 >------->-------(vmbus_proto_version == VERSION_WIN7)) {

 >------->-------/* Since we are a child, we only need to check bit 0 */
 >------->-------if (sync_test_and_clear_bit(0, event->flags))
 >------->------->-------handled = true;
 >-------} else {
 >------->-------/*
 >------->------- * Our host is win8 or above. The signaling mechanism
 >------->------- * has changed and we can directly look at the event page.
 >------->------- * If bit n is set then we have an interrup on the channel
 >------->------- * whose id is n.
 >------->------- */
 >------->-------handled = true;
 >-------}

 >-------if (handled)
 >------->-------vmbus_chan_sched(hv_cpu);

 >-------page_addr = hv_cpu->synic_message_page;
 >-------msg = (struct hv_message *)page_addr + VMBUS_MESSAGE_SINT;

 >-------/* Check if there are actual msgs to be processed */
 >-------if (msg->header.message_type != HVMSG_NONE) {
 >------->-------if (msg->header.message_type == HVMSG_TIMER_EXPIRED)
 >------->------->-------hv_process_timer_expiration(msg, hv_cpu);
 >------->-------else
 >------->------->-------tasklet_schedule(&hv_cpu->msg_dpc);
 >-------}

 >-------add_interrupt_randomness(HYPERVISOR_CALLBACK_VECTOR, 0);
 }

hv_cpu->msg_dpc这个tasklet是在hv_synic_alloc函数中初始化的。这个函数对hv_context进行了初始化.我们可以看到msg_dpc 这个tasklet最后将要执行的函数是 vmbus_on_msg_dpc 函数,需要注意这里又一个percpu的tasklet,表示这个类型的tasklet可以并发执行,相当于搞了一个软中断号。可以看到这里创建了一个workqueue , workqueue里面执行的是vmbus_onmessage_work函数,vmbus_onmessage_work函数里面直接调用 vmbus_onmessage函数。

 void vmbus_on_msg_dpc(unsigned long data)
 {
 >-------struct hv_per_cpu_context *hv_cpu = (void *)data;
 >-------void *page_addr = hv_cpu->synic_message_page;
 >-------struct hv_message *msg = (struct hv_message *)page_addr +
 >------->------->------->-------  VMBUS_MESSAGE_SINT;
 >-------struct vmbus_channel_message_header *hdr;
 >-------const struct vmbus_channel_message_table_entry *entry;
 >-------struct onmessage_work_context *ctx;
 >-------u32 message_type = msg->header.message_type;

 >-------if (message_type == HVMSG_NONE)
 >------->-------/* no msg */
 >------->-------return;

 >-------hdr = (struct vmbus_channel_message_header *)msg->u.payload;

 >-------trace_vmbus_on_msg_dpc(hdr);

 >-------if (hdr->msgtype >= CHANNELMSG_COUNT) {
 >------->-------WARN_ONCE(1, "unknown msgtype=%d\n", hdr->msgtype);
 >------->-------goto msg_handled;
 >-------}

 >-------entry = &channel_message_table[hdr->msgtype];
 >-------if (entry->handler_type>== VMHT_BLOCKING) {
 >------->-------ctx = kmalloc(sizeof(*ctx), GFP_ATOMIC);
 >------->-------if (ctx == NULL)
 >------->------->-------return;

 >------->-------INIT_WORK(&ctx->work, vmbus_onmessage_work);
 >------->-------memcpy(&ctx->msg, msg, sizeof(*msg));

 >------->-------/*
 >------->------- * The host can generate a rescind message while we
 >------->------- * may still be handling the original offer. We deal with
 >------->------- * this condition by ensuring the processing is done on the
 >------->------- * same CPU.
 >------->------- */
 >------->-------switch (hdr->msgtype) {
 >------->-------case CHANNELMSG_RESCIND_CHANNELOFFER:
 >------->------->-------/*
 >------->------->------- * If we are handling the rescind message;
 >------->------->------- * schedule the work on the global work queue.
 >------->------->------- */
 >------->------->-------schedule_work_on(vmbus_connection.connect_cpu,
 >------->------->------->------->------- &ctx->work);
 >------->------->-------break;

 >------->-------case CHANNELMSG_OFFERCHANNEL:
 >------->------->-------atomic_inc(&vmbus_connection.offer_in_progress);
 >------->------->-------queue_work_on(vmbus_connection.connect_cpu,
 >------->------->------->-------      vmbus_connection.work_queue,
 >------->------->------->-------      &ctx->work);
 >------->------->-------break;

 >------->-------default:
 >------->------->-------queue_work(vmbus_connection.work_queue, &ctx->work);
 >------->-------}
 >-------} else
 >------->-------entry->message_handler(hdr);

 msg_handled:
 >-------vmbus_signal_eom(msg, message_type);
 }

vmbus_onmessage函数内容如下,这个函数前面又一个message table 用来路由消息的处理函数。

const struct vmbus_channel_message_table_entry
 channel_message_table[CHANNELMSG_COUNT] = {
 >-------{ CHANNELMSG_INVALID,>-->------->-------0, NULL },
 >-------{ CHANNELMSG_OFFERCHANNEL,>----->-------0, vmbus_onoffer },
 >-------{ CHANNELMSG_RESCIND_CHANNELOFFER,>-----0, vmbus_onoffer_rescind },
 >-------{ CHANNELMSG_REQUESTOFFERS,>---->-------0, NULL },
 >-------{ CHANNELMSG_ALLOFFERS_DELIVERED,>------1, vmbus_onoffers_delivered },
 >-------{ CHANNELMSG_OPENCHANNEL,>------>-------0, NULL },
 >-------{ CHANNELMSG_OPENCHANNEL_RESULT,>-------1, vmbus_onopen_result },
 >-------{ CHANNELMSG_CLOSECHANNEL,>----->-------0, NULL },
 >-------{ CHANNELMSG_GPADL_HEADER,>----->-------0, NULL },
 >-------{ CHANNELMSG_GPADL_BODY,>------->-------0, NULL },
 >-------{ CHANNELMSG_GPADL_CREATED,>---->-------1, vmbus_ongpadl_created },
 >-------{ CHANNELMSG_GPADL_TEARDOWN,>--->-------0, NULL },
 >-------{ CHANNELMSG_GPADL_TORNDOWN,>--->-------1, vmbus_ongpadl_torndown },
 >-------{ CHANNELMSG_RELID_RELEASED,>--->-------0, NULL },
 >-------{ CHANNELMSG_INITIATE_CONTACT,>->-------0, NULL },
 >-------{ CHANNELMSG_VERSION_RESPONSE,>->-------1, vmbus_onversion_response },
 >-------{ CHANNELMSG_UNLOAD,>--->------->-------0, NULL },
 >-------{ CHANNELMSG_UNLOAD_RESPONSE,>-->-------1, vmbus_unload_response },
 >-------{ CHANNELMSG_18,>------->------->-------0, NULL },
 >-------{ CHANNELMSG_19,>------->------->-------0, NULL },
 >-------{ CHANNELMSG_20,>------->------->-------0, NULL },
 >-------{ CHANNELMSG_TL_CONNECT_REQUEST,>-------0, NULL },
 };

 /*
  * vmbus_onmessage - Handler for channel protocol messages.
  *
  * This is invoked in the vmbus worker thread context.
  */
 void vmbus_onmessage(void *context)
 {
 >-------struct hv_message *msg = context;
 >-------struct vmbus_channel_message_header *hdr;
 >-------int size;

 >-------hdr = (struct vmbus_channel_message_header *)msg->u.payload;
 >-------size = msg->header.payload_size;

 >-------trace_vmbus_on_message(hdr);

 >-------if (hdr->msgtype >= CHANNELMSG_COUNT) {
 >------->-------pr_err("Received invalid channel message type %d size %d\n",
 >------->------->-------   hdr->msgtype, size);
 >------->-------print_hex_dump_bytes("", DUMP_PREFIX_NONE,
 >------->------->------->-------     (unsigned char *)msg->u.payload, size);
 >------->-------return;
 >-------}

 >-------if (channel_message_table[hdr->msgtype].message_handler)
 >------->-------channel_message_table[hdr->msgtype].message_handler(hdr);
 >-------else
 >------->-------pr_err("Unhandled channel message type %d\n", hdr->msgtype);
 }

在这些消息处理函数里面又一个vmbus_onoffer处理函数,这个函数会创建新的channel,并且调用vmbus_process_offer函数,这个函数会判断当前channel是primary还是sub channel,并设置相应的关系。最后会创建workqueue 调用 vmbus_add_channel_work 函数。vmbus_add_channel函数如下

 static void vmbus_add_channel_work(struct work_struct *work)
 {
 >-------struct vmbus_channel *newchannel =
 >------->-------container_of(work, struct vmbus_channel, add_channel_work);
 >-------struct vmbus_channel *primary_channel = newchannel->primary_channel;
 >-------unsigned long flags;
 >-------u16 dev_type;
 >-------int ret;

 >-------dev_type = hv_get_dev_type(newchannel);

 >-------init_vp_index(newchannel, dev_type);

 >-------if (newchannel->target_cpu != get_cpu()) {
 >------->-------put_cpu();
 >------->-------smp_call_function_single(newchannel->target_cpu,
 >------->------->------->------->------- percpu_channel_enq,
 >------->------->------->------->------- newchannel, true);
 >-------} else {
 >------->-------percpu_channel_enq(newchannel);
 >------->-------put_cpu();
 >-------}

 >-------/*
 >------- * This state is used to indicate a successful open
 >------- * so that when we do close the channel normally, we
 >------- * can cleanup properly.
 >------- */
 >-------newchannel->state = CHANNEL_OPEN_STATE;

 >-------if (primary_channel != NULL) {
 >------->-------/* newchannel is a sub-channel. */
 >------->-------struct hv_device *dev = primary_channel->device_obj;

 >------->-------if (vmbus_add_channel_kobj(dev, newchannel))
 >------->------->-------goto err_deq_chan;

 >------->-------if (primary_channel->sc_creation_callback != NULL)
 >------->------->-------primary_channel->sc_creation_callback(newchannel);

 >------->-------newchannel->probe_done = true;
 >------->-------return;
 >-------}

 >-------/*
 >------- * Start the process of binding the primary channel to the driver
 >------- */
 >-------newchannel->device_obj = vmbus_device_create(
 >------->-------&newchannel->offermsg.offer.if_type,
 >------->-------&newchannel->offermsg.offer.if_instance,
 >------->-------newchannel);
 >-------if (!newchannel->device_obj)
 >------->-------goto err_deq_chan;

 >-------newchannel->device_obj->device_id = dev_type;
 >-------/*
 >------- * Add the new device to the bus. This will kick off device-driver
 >------- * binding which eventually invokes the device driver's AddDevice()
 >------- * method.
 >------- */
 >-------ret = vmbus_device_register(newchannel->device_obj);

 >-------if (ret != 0) {
 >------->-------pr_err("unable to add child device object (relid %d)\n",
 >------->------->-------newchannel->offermsg.child_relid);
 >------->-------kfree(newchannel->device_obj);
 >------->-------goto err_deq_chan;
 >-------}

 >-------newchannel->probe_done = true;
 >-------return;

 err_deq_chan:
 >-------mutex_lock(&vmbus_connection.channel_mutex);

 >-------/*
 >------- * We need to set the flag, otherwise
 >------- * vmbus_onoffer_rescind() can be blocked.
 >------- */
 >-------newchannel->probe_done = true;

 >-------if (primary_channel == NULL) {
 >------->-------list_del(&newchannel->listentry);
 >-------} else {
 >------->-------spin_lock_irqsave(&primary_channel->lock, flags);
 >------->-------list_del(&newchannel->sc_list);
 >------->-------spin_unlock_irqrestore(&primary_channel->lock, flags);
 >-------}

 >-------mutex_unlock(&vmbus_connection.channel_mutex);

 >-------if (newchannel->target_cpu != get_cpu()) {
 >------->-------put_cpu();
 >------->-------smp_call_function_single(newchannel->target_cpu,
 >------->------->------->------->------- percpu_channel_deq,
 >------->------->------->------->------- newchannel, true);
 >-------} else {
 >------->-------percpu_channel_deq(newchannel);
 >------->-------put_cpu();
 >-------}

 >-------vmbus_release_relid(newchannel->offermsg.child_relid);

 >-------free_channel(newchannel);
 }

vmbus_add_channel_work函数的 对channel进行操作,不同的类型的channel操作不一样,对于primary的channel 会调用 vmbus_device_register注册一个新的vmbus 设备,对于sub channel则会调用 sc_creation_callback函数 设置任务。这块会在网卡注册的时候讲。注意一个小细节 如果当前的cpu不等于 channel的target_cpu则会调用ipi 把当前channel 加到target cpu的hv_cpu_context链表中.target_cpu是在init_vp_index中计算出来的。

vmbus_device_rgister注册后,会触发 match 以及调用 vmbus上的驱动操作,驱动虚拟设备。

有上面我们发现 vmbus上的设备创建是由 hyper-V 给虚拟机发送中断,然后发消息,然后虚拟机中的vmbus 创建 这个设备。 这个方式非常新颖。相对于pci枚举或者其他方式,这种方式把外设当作一个主体,相当于可编程的外设。个人感觉学到了

分类
未分类

TCP listen 过程分析

TCP listen 系统调用过程
__sys__listen –> inet_listen –>inet_csk_listen_start–>inet_hash–>__inet_hash
1.在 inet_listen函数中,检查sock状态是否为 TCPF_CLOSE或者TCPF_LISTEN状态,只有这两个状态的sock才能listen。如果已经处于listen状态,则只修改backlog,否则需要执行inet_csk_listen_start函数将sock添加到listen hash table中
2.inet_csk_listen_start函数会改变sock状态,改为TCP_LISTEN状态。
3.inet_hash函数将sock 添加到listen hash table中,备查。如果设置了sk_reuseport标志位,则会调用inet_reuseport_add_sock函数处理reuseport
具体细节可以沿着这个调用栈看代码。

TCP listen 收到 syn包后的执行的动作

tcp_v4_rcv –> tcp_v4_do_rcv –> tcp_rcv_state_process –> tcp_v4_conn_request –> tcp_conn_request –>tcp_v4_send_synack

1.tcp_v4_rcv 函数在listen hash table中找到对应的 sock。调用 tcp_v4_do_rcv函数
2.tcp_v4_do_rcv 函数判断有没有开启tcp cookie。在收到syn 包的过程中 cookie开启不开启没有影响
3.tcp_rcv_state_process函数根据sock状态及 来包状态,决定调用tcp_connect_request函数。
4.tcp_conn_request 函数
a.如果没有开启tcp cookie和fastopen的情况下,会创建一个request_sock的sock,并将sk_state设置为 TCP_NEW_SYN_RECV状态。
b.调用inet_csk_reqsk_queue_hash_add将 req sock 添加到 establish hash table中,并发送 synack。

TCP listen 收到 ack执行动作。
tcp_v4_rcv –> tcp_check_req –> tcp_v4_syn_recv_sock–>tcp_create_openreq_child–>inet_csk_clone_lock
1.tcp_check_req 函数检查ack包是否有问题,并将tcp_v4_syn_recv_sock创建的sock 通过inet_csk_reqsk_queue_add放到 accept队列中。
2.tcp_v4_syn_recv_sock 函数创建child sock。
3. inet_csk_cloen_lock函数会设置child sock 状态为TCP_SYN_RECV
4.最后调用 tcp_child_process,对ack 进行处理,当sock正在被调用时 把包放到backlog里面,否则唤醒listen的进程。
我分析的过程中 忽略了 tcp cookie 和 fastopen。不过主线流程情况以后,这些附加功能可以慢慢通过看代码了解。

分类
未分类

tcp bind 端口分析

bind系统调用 是指定源端口的。 大多数情况下 我们在 listen之前调用,但是在connect之前也可以调用这个系统调用,指定发起连接的源端口。
在TCP bind系统调用中,会创建一个 inet_bind_bucket的实体。系统会把这个结构记录在一个hash表中,后续bind时使用它来判定是否可以bind。
在以下四种情况下 可以共享本地端口
1. sockets bound 到了不同的 接口上。
2.sockets 设置了 REUSEADDR,且没有sockets处于listen状态。
3.sockets 设置了REUSEADDR,且有sockets处于listen状态,如果还设置了REUSEPORT也可以共享。
4.所有的sockets 绑定在不同的地址上。
判断是否有冲突的函数是 inet_csk_bind_conflict,有兴趣可以进去仔细看。

需要注意的是,除了在bind 函数中会创建inet_bind_bucket的实体外,如果在connect前没有进行bind,同样会创建inet_bind_bucket实体,并将该实体插入 bhash hash表中。只是在connect判断端口是否可以复用采用了更加简单地判定方法。具体可以看__inet_hash_connect函数。
bind 系统调用的调用链如下

__sys_bind –> inet_bind –> __inet_bind –> inet_csk_get_port(get_port) –> inet_csk_bind_conflict

bind 关键点在于判定 端口是否可用。判定条件是前文中提到的四个。

分类
未分类

TCP connect 收到 syn/ack 处理流程

在上章中分析了connect流程。这一章分析,服务端发送syn/ack后,客户端的处理流程。
这个流程在IPV4中的入口函数是tcp_v4_rcv.当ip层收到tcp包后,会将这种包上送到tcp_v4_rcv函数中。
这个函数首先检查数据包的合法性(数据包校验和是否正确,数据包是否完整)
然后在tcp 连接表中查找 sock。首先在established表中查找,如果在established表中差找不到,则在listener表中查找。由于我们分析的是syn/ack收包过程,这个sk应该会在established表中查找到。且sock 状态为TCP_SYN_SENT。

        if (!sock_owned_by_user(sk)) {
                ret = tcp_v4_do_rcv(sk, skb);   //tcp_v4_rcv函数最后调用这个函数处理syn/ack报文
        } else if (tcp_add_backlog(sk, skb)) {
                goto discard_and_relse;
        }

在tcp_v4_do_rcv函数中,会根据sk状态执行不同的操作,由于当前sk状态为TCP_SYN_SENT,因此直接执行tcp_rcv_state_process函数。

        if (tcp_rcv_state_process(sk, skb)) { //改变tcp sk状态函数。这个函数根据当前状态以及skb包中标志位,改变sk状态
                rsk = sk;
                goto reset;
        }

在tcp_rcv_state_process函数中,根据当前TCP sk 状态 为TCP_SYN_SENT,执行的代码为

        case TCP_SYN_SENT:
                tp->rx_opt.saw_tstamp = 0; 
                tcp_mstamp_refresh(tp);
                queued = tcp_rcv_synsent_state_process(sk, skb, th);  //执行这个函数改变tcp状态
                if (queued >= 0)
                        return queued;

                /* Do step6 onward by hand. */
                tcp_urg(sk, skb, th); 
                __kfree_skb(skb);
                tcp_data_snd_check(sk);
                return 0;
        } 

tcp_rcv_synsent_state_process首先判断是否有 ack报文,然后检查报文的合法性,(seq,tstamp)。然后检查是否有rst,如果有rst 则直接关闭socket,并通知用户态程序 该sock 出错。

        if (th->ack) {
                /* rfc793:
                 * "If the state is SYN-SENT then
                 *    first check the ACK bit
                 *      If the ACK bit is set
                 *        If SEG.ACK =< ISS, or SEG.ACK > SND.NXT, send
                 *        a reset (unless the RST bit is set, if so drop
                 *        the segment and return)"
                 */
                if (!after(TCP_SKB_CB(skb)->ack_seq, tp->snd_una) ||
                    after(TCP_SKB_CB(skb)->ack_seq, tp->snd_nxt))
                        goto reset_and_undo;

                if (tp->rx_opt.saw_tstamp && tp->rx_opt.rcv_tsecr &&
                    !between(tp->rx_opt.rcv_tsecr, tp->retrans_stamp,
                             tcp_time_stamp(tp))) {
                        NET_INC_STATS(sock_net(sk),
                                        LINUX_MIB_PAWSACTIVEREJECTED);
                        goto reset_and_undo;
                }

                /* Now ACK is acceptable.
                 *
                 * "If the RST bit is set
                 *    If the ACK was acceptable then signal the user "error:
                 *    connection reset", drop the segment, enter CLOSED state,
                 *    delete TCB, and return."
                 */

                if (th->rst) {
                        tcp_reset(sk);
                        goto discard;
                }

                /* rfc793:
                 *   "fifth, if neither of the SYN or RST bits is set then
                 *    drop the segment and return."
                 *
                 *    See note below!
                 *                                        --ANK(990513)
                 */
                if (!th->syn)
                        goto discard_and_undo;

                /* rfc793:
                 *   "If the SYN bit is on ...
                 *    are acceptable then ...
                 *    (our SYN has been ACKed), change the connection
                 *    state to ESTABLISHED..."
                 */

                tcp_ecn_rcv_synack(tp, th);

                tcp_init_wl(tp, TCP_SKB_CB(skb)->seq);
                tcp_ack(sk, skb, FLAG_SLOWPATH);

                /* Ok.. it's good. Set up sequence numbers and
                 * move to established.
                 */
                tp->rcv_nxt = TCP_SKB_CB(skb)->seq + 1;
                tp->rcv_wup = TCP_SKB_CB(skb)->seq + 1;

                /* RFC1323: The window in SYN & SYN/ACK segments is
                 * never scaled.
                 */
                tp->snd_wnd = ntohs(th->window);

如果整个过程中一切都ok,则调用 tcp_finish_connect函数,这个函数将tcp sk 状态改为 TCP_ESTABLISH状态,如果开启了keepalive则设置keepalive定时器。代码如下

void tcp_finish_connect(struct sock *sk, struct sk_buff *skb)
{
        struct tcp_sock *tp = tcp_sk(sk);
        struct inet_connection_sock *icsk = inet_csk(sk);

        tcp_set_state(sk, TCP_ESTABLISHED);
        icsk->icsk_ack.lrcvtime = tcp_jiffies32;
        if (skb) {
                icsk->icsk_af_ops->sk_rx_dst_set(sk, skb);
                security_inet_conn_established(sk, skb);
                sk_mark_napi_id(sk, skb);
        }
        tcp_init_transfer(sk, BPF_SOCK_OPS_ACTIVE_ESTABLISHED_CB);

        /* Prevent spurious tcp_cwnd_restart() on first data
         * packet.
         */
        tp->lsndtime = tcp_jiffies32;

        if (sock_flag(sk, SOCK_KEEPOPEN))
                inet_csk_reset_keepalive_timer(sk, keepalive_time_when(tp));

        if (!tp->rx_opt.snd_wscale)
                __tcp_fast_path_on(tp, tp->snd_wnd);
        else
                tp->pred_flags = 0;
}

如果收到syn/rst 则会调用 tcp_reset函数。函数如下

void tcp_reset(struct sock *sk)
{
        trace_tcp_receive_reset(sk);
        /* We want the right error as BSD sees it (and indeed as we do). */
        switch (sk->sk_state) {
        case TCP_SYN_SENT:
                sk->sk_err = ECONNREFUSED;
                break;
        case TCP_CLOSE_WAIT:
                sk->sk_err = EPIPE;
                break;
        case TCP_CLOSE:
                return;
        default:
                sk->sk_err = ECONNRESET;
        }
        /* This barrier is coupled with smp_rmb() in tcp_poll() */
        smp_wmb();

        tcp_write_queue_purge(sk);
        tcp_done(sk);

        if (!sock_flag(sk, SOCK_DEAD))
                sk->sk_error_report(sk);  //通知用户态程序出错,具体函数是sock_def_error_report。 在net/core/sock.c文件中
}

sock_def_error_report函数如下:

static void sock_def_error_report(struct sock *sk)
{
        struct socket_wq *wq;

        rcu_read_lock();
        wq = rcu_dereference(sk->sk_wq);
        if (skwq_has_sleeper(wq))
                wake_up_interruptible_poll(&wq->wait, EPOLLERR);
        sk_wake_async(sk, SOCK_WAKE_IO, POLL_ERR); //正常情况下返回POLL_ERR,但是我之前遇到过既返回POLL_ERR又返回POLL_IN POLL_OUT的情况,可能是因为有其他事件同时发生导致。
        rcu_read_unlock();
}

最后调用 sk_wake_async唤醒阻塞在sk上的进程。

                if (!sock_flag(sk, SOCK_DEAD)) {
                        sk->sk_state_change(sk);
                        sk_wake_async(sk, SOCK_WAKE_IO, POLL_OUT);
                }

最后调用 tcp_send_ack 发送 ack报文到服务端,整个tcp 接受syn/ack 过程完成。
后面的函数都是一些异常处理。

分类
未分类

通过dlopen加载动态库,注册的四种方式。

看内核代码,发现在内核在加载module时将 全局的结构体变量注册到链表中有以下几种方式。总结以下
第一种 方式,在so模块中专门写一个函数用来将本模块中的全局变量注册到链表中。例子

#include <stdio.h>
#include "main.h"
static void print_func()
{
    printf("%s\n","test_so1");
}
static struct so_funcs my_func = {
    .name="test_so1",
    .do_func=print_func,
};
void init_test_so1()
{
    test_funcs[0]=&my_func;
}

方式, 是dlopen打开,以后调用dlsym 找到 init_test_so1函数地址,并执行这个函数。
第二种方式,通过constructor attribute,在加载so时自动执行init函数,代码如下

#include <stdio.h>
#include "main.h"
static void print_func()
{
    printf("%s\n","test_so2");
}
static struct so_funcs my_func = {
    .name="test_so2",
    .do_func=print_func,
};
void __attribute__((constructor)) init_test_so2()
{
    test_funcs[1]=&my_func;
}

由于有attribute((constructor)) ,dlopen在打开so库时,会自动执行init_test_so2函数将全局变量注册到链表中。
第三种方式,与第一中方式类似,第一种方式通过dlsym找到函数符号地址,执行函数,第三种找到符号变量,然后注册到链表中,在查找符号变量时 有两种方式,第一种,通过符号变量名查找变量地址,第二种通过将变量放在特殊的section中,通过section查找地址。例子如下

#include <stdio.h>
#include "main.h"
static void print_func()
{
    printf("%s\n","test_so3");
}
static struct so_funcs my_func = {
    .name="test_so3",
    .do_func=print_func,
};
static struct so_funcs __attribute__((section(".my_sections"))) *module_func = &my_func;//放在特殊的section中 my_sections.在主程序中会利用这个查找
struct so_funcs __attribute__((section(".my_sections"))) *module_func1 = &my_func;

main函数如下

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <link.h>
#include <dlfcn.h>
#include "main.h"
#define test1_path "./libtest_so1.so"
#define test2_path "./libtest_so2.so"
#define test3_path "./libtest_so3.so"
struct so_funcs *test_funcs[3];
void analysis_test3(void *test3)
{
    struct link_map *map;
    dlinfo(test3, RTLD_DI_LINKMAP, &map);
    printf("%p: %s: %p\n", (void *)map->l_addr, map->l_name, map->l_ld);
    test_funcs[2] = *(struct so_funcs **)((unsigned long)(map->l_ld)+(unsigned long)(0x1060-0xe18)); //注意 这个地方的0x1060 和0xe18是通过objdump -h libtest_so3.so查到的。可以做到自动化查询,这只是一个简单的例子没有做。
    printf("----------%p\n",test_funcs[2]);

}
int main()
{
    void *test1_handle=NULL;
    void *test2_handle=NULL;
    void *test3_handle=NULL;
    void (*register_func)();
    int i=0;
    for (i=0;i<3;i++)
        test_funcs[i]=NULL;
    test1_handle = dlopen(test1_path,RTLD_LAZY|RTLD_GLOBAL);
    printf("address = %p\n",test1_handle);
    printf("error %s\n",dlerror());
    test2_handle = dlopen(test2_path,RTLD_LAZY|RTLD_GLOBAL);
    printf("address = %p\n",test2_handle);
    printf("error %s\n",dlerror());
    test3_handle = dlopen(test3_path,RTLD_LAZY|RTLD_GLOBAL);
    printf("address = %p\n",test3_handle);
    printf("error %s\n",dlerror());
    register_func=(void(*)())dlsym(test1_handle,"init_test_so1");
    printf("address is %p\n",register_func);
    printf("error %s\n",dlerror());
    register_func();
    analysis_test3(test3_handle);
    for(i=0;i<3;i++){
        if(test_funcs[i]!=NULL){
            printf("funcs %s is run\n",test_funcs[i]->name);
            test_funcs[i]->do_func();
        }else{
            printf("funcs %d is NULL\n",i);
        }
    }
    struct so_funcs *test33=*(struct so_funcs **)dlsym(test3_handle,"module_func1");
    printf("last address %p\n",test33);
    return 0;
}

main.c文件非常,唯一需要注意的就是如何查找 指定section的首地址的问题,本例子简单的使用了 dynamic section 偏移和my_sections偏移,以及指导dynamic内存中的地址后计算的。现在是通过手工计算出来的,可以做成自动化。
Makefile 如下

all:libtest_so1.so libtest_so2.so libtest_so3.so
    gcc -rdynamic -o main main.c -ldl 

libtest_so1.so:test_so1.c
    gcc -c -fPIC test_so1.c
    gcc test_so1.o -shared -o libtest_so1.so

libtest_so2.so:test_so2.c
    gcc -c -fPIC test_so2.c
    gcc test_so2.o -shared -o libtest_so2.so

libtest_so3.so:test_so3.c
    gcc -c -fPIC test_so3.c
    gcc test_so3.o -shared -o libtest_so3.so
clean:
    rm -rf *.o *.so main

注意在编译main 程序时必须加上rdynamic 否则在调用dlopen时会报错找不到 main.c中定义的全局变量

分类
未分类

kernel trace_event 宏展开注解

要理解kernel的trace_event机制,最好的方式是读懂 samples/trace_events/trace_events_sample的例子代码。这个例子的代码非常难以读懂。我是按照以下三个阶段读懂的。怎么申明和定义的,怎么注册到系统中的,怎么使能的。
在这个例子里面有一个非常有用的宏,这一个宏展开即完成了trace_event的申明和定义,看例子时,我们只看foo_bar这个trace_event的流程。
首先,我们需要看trace-events-sample.h这个头文件,这个头文件比较怪异。可以被多次包含。在头文件的最后还包含了另外一个头文件trace/define_trace.h。包含这个头文件是魔法开始的地方。
下面开始分析trace-events-sample.h头文件
trace-events-sample.h头文件在开始linux/tracepoint.h头文件,注意这个头文件只能被包含一次。不能重复包含 被 ifndef endif保护。
紧接着写了一个宏如下。

TRACE_EVENT(foo_bar,

        TP_PROTO(const char *foo, int bar, const int *lst,
                 const char *string, const struct cpumask *mask),

        TP_ARGS(foo, bar, lst, string, mask),

        TP_STRUCT__entry(
                __array(        char,   foo,    10              )
                __field(        int,    bar                     )
                __dynamic_array(int,    list,   __length_of(lst))
                __string(       str,    string                  )
                __bitmask(      cpus,   num_possible_cpus()     )
        ),

        TP_fast_assign(
                strlcpy(__entry-&gt;foo, foo, 10);
                __entry-&gt;bar    = bar;
                memcpy(__get_dynamic_array(list), lst,
                       __length_of(lst) * sizeof(int));
                __assign_str(str, string);
                __assign_bitmask(cpus, cpumask_bits(mask), num_possible_cpus());
        ),

        TP_printk(&quot;foo %s %d %s %s %s %s (%s)&quot;, __entry-&gt;foo, __entry-&gt;bar,

/*
 * Notice here the use of some helper functions. This includes:
 *
 *  __print_symbolic( variable, { value, &quot;string&quot; }, ... ),
 *
 *    The variable is tested against each value of the { } pair. If
 *    the variable matches one of the values, then it will print the
 *    string in that pair. If non are matched, it returns a string
 *    version of the number (if __entry-&gt;bar == 7 then &quot;7&quot; is returned).
 */
                  __print_symbolic(__entry-&gt;bar,
                                   { 0, &quot;zero&quot; },
                                   { TRACE_SAMPLE_FOO, &quot;TWO&quot; },
                                   { TRACE_SAMPLE_BAR, &quot;FOUR&quot; },
                                   { TRACE_SAMPLE_ZOO, &quot;EIGHT&quot; },
                                   { 10, &quot;TEN&quot; }
                          ),

/*
 *  __print_flags( variable, &quot;delim&quot;, { value, &quot;flag&quot; }, ... ),
 *
 *    This is similar to __print_symbolic, except that it tests the bits
 *    of the value. If ((FLAG &amp; variable) == FLAG) then the string is
 *    printed. If more than one flag matches, then each one that does is
 *    also printed with delim in between them.
 *    If not all bits are accounted for, then the not found bits will be
 *    added in hex format: 0x506 will show BIT2|BIT4|0x500
 */
                  __print_flags(__entry-&gt;bar, &quot;|&quot;,
                                { 1, &quot;BIT1&quot; },
                                { 2, &quot;BIT2&quot; },
                                { 4, &quot;BIT3&quot; },
                                { 8, &quot;BIT4&quot; }
                          ),
/*
 *  __print_array( array, len, element_size )
 *
 *    This prints out the array that is defined by __array in a nice format.
 */
                  __print_array(__get_dynamic_array(list),
                                __get_dynamic_array_len(list) / sizeof(int),
                                sizeof(int)),
                  __get_str(str), __get_bitmask(cpus))
);

在linux/tracepoint.h头文件中对这个宏有定义,宏展开方式按照linux/tracepoint.h中定义的展开

#define TRACE_EVENT(name, proto, args, struct, assign, print)   \
        DECLARE_TRACE(name, PARAMS(proto), PARAMS(args))

再看DECLARE_TRACE宏展开

#define DECLARE_TRACE(name, proto, args)                                \
        __DECLARE_TRACE(name, PARAMS(proto), PARAMS(args),              \
                        cpu_online(raw_smp_processor_id()),             \
                        PARAMS(void *__data, proto),                    \
                        PARAMS(__data, args))

看 __DECLARE_TRACE展开

#define __DECLARE_TRACE(name, proto, args, cond, data_proto, data_args) \
        extern struct tracepoint __tracepoint_##name;                   \
        static inline void trace_##name(proto)                          \  //这个函数是在需要trace的地方插入,例如例子中在需要trace的地方写了trace_foo_bar
        {                                                               \
                if (static_key_false(&amp;__tracepoint_##name.key))         \
                        __DO_TRACE(&amp;__tracepoint_##name,                \
                                TP_PROTO(data_proto),                   \
                                TP_ARGS(data_args),                     \
                                TP_CONDITION(cond), 0);                 \
                if (IS_ENABLED(CONFIG_LOCKDEP) &amp;&amp; (cond)) {             \
                        rcu_read_lock_sched_notrace();                  \
                        rcu_dereference_sched(__tracepoint_##name.funcs);\
                        rcu_read_unlock_sched_notrace();                \
                }                                                       \
        }                                                               \
        __DECLARE_TRACE_RCU(name, PARAMS(proto), PARAMS(args),          \
                PARAMS(cond), PARAMS(data_proto), PARAMS(data_args))    \
        static inline int                                               \
        register_trace_##name(void (*probe)(data_proto), void *data)    \ //注册探测函数,tracepoint会用到 trace_event中不用这个方式注册
        {                                                               \
                return tracepoint_probe_register(&amp;__tracepoint_##name,  \
                                                (void *)probe, data);   \
        }                                                               \
        static inline int                                               \
        register_trace_prio_##name(void (*probe)(data_proto), void *data,\
                                   int prio)                            \
        {                                                               \
                return tracepoint_probe_register_prio(&amp;__tracepoint_##name, \
                                              (void *)probe, data, prio); \
        }                                                               \
        static inline int                                               \
        unregister_trace_##name(void (*probe)(data_proto), void *data)  \
        {                                                               \
                return tracepoint_probe_unregister(&amp;__tracepoint_##name,\
                                                (void *)probe, data);   \
        }                                                               \
        static inline void                                              \
        check_trace_callback_type_##name(void (*cb)(data_proto))        \
        {                                                               \
        }                                                               \
        static inline bool                                              \
        trace_##name##_enabled(void)                                    \//使能探测,同样单独使用tracepoint时需要,在使用trace_event时不需要使用这个函数。
        {                                                               \
                return static_key_false(&amp;__tracepoint_##name.key);      \
        }
#define DEFINE_TRACE_FN(name, reg, unreg)                                \
        static const char __tpstrtab_##name[]                            \
        __attribute__((section(&quot;__tracepoints_strings&quot;))) = #name;       \
        struct tracepoint __tracepoint_##name                            \  //tracepoint 结构体定义。
        __attribute__((section(&quot;__tracepoints&quot;))) =                      \
                { __tpstrtab_##name, STATIC_KEY_INIT_FALSE, reg, unreg, NULL };\
        static struct tracepoint * const __tracepoint_ptr_##name __used  \
        __attribute__((section(&quot;__tracepoints_ptrs&quot;))) =                 \
                &amp;__tracepoint_##name;

#define DEFINE_TRACE(name)                                              \
        DEFINE_TRACE_FN(name, NULL, NULL);

以上都很常规,真正魔法开始的地方在下面

#define TRACE_INCLUDE_PATH .        //定义头文件路径,在define_trace.h中会用到
/*
 * TRACE_INCLUDE_FILE is not needed if the filename and TRACE_SYSTEM are equal
 */
#define TRACE_INCLUDE_FILE trace-events-sample          //定义头文件名称,在define_trace.h中会用到
#include &lt;trace/define_trace.h&gt;           //包含trace/define_trace.h

trace/define_trace.h文件在开始就对TRACE_EVENT宏进行了重定义

#undef TRACE_EVENT
#define TRACE_EVENT(name, proto, args, tstruct, assign, print)  \
        DEFINE_TRACE(name)

接下来这一段如下,

#ifndef TRACE_INCLUDE_PATH
# define __TRACE_INCLUDE(system) &lt;trace/events/system.h&gt;
# define UNDEF_TRACE_INCLUDE_PATH
#else
# define __TRACE_INCLUDE(system) __stringify(TRACE_INCLUDE_PATH/system.h)
#endif

# define TRACE_INCLUDE(system) __TRACE_INCLUDE(system)

/* Let the trace headers be reread */
#define TRACE_HEADER_MULTI_READ

#include TRACE_INCLUDE(TRACE_INCLUDE_FILE)

这段代码的大意 就是重新包含TRACE_INCLUDE_FIELE也就是重新包含trace-events-sample.h,重新展开TRACE_EVENT宏。这段展开是tracepoint定义
define_trace.h包trace_events.h 这个文件是trace_event的核心。

#include &lt;trace/trace_events.h&gt;

在trace_events.h 头文件中,在一开始又重新定义了 TRACE_EVENT宏。

#undef TRACE_EVENT
#define TRACE_EVENT(name, proto, args, tstruct, assign, print) \
        DECLARE_EVENT_CLASS(name,                              \
                             PARAMS(proto),                    \
                             PARAMS(args),                     \
                             PARAMS(tstruct),                  \
                             PARAMS(assign),                   \
                             PARAMS(print));                   \
        DEFINE_EVENT(name, name, PARAMS(proto), PARAMS(args));

在剩下的代码中会数次重新定义DECLARE_EVENT_CLASS 和 DEFINE_EVENT宏,然后重新包含trace-events-sample.h。对TRACE_EVENT宏进行7次展开。中间的展开很简单就不一一说明了。
直接看最后一次展开。

 #undef DECLARE_EVENT_CLASS
 #define DECLARE_EVENT_CLASS(call, proto, args, tstruct, assign, print)         \
 _TRACE_PERF_PROTO(call, PARAMS(proto));                                                \
 static char print_fmt_##call[] = print;                                                                \
 static struct trace_event_class __used __refdata event_class_##call = {                \
                .system                         = TRACE_SYSTEM_STRING,                      \
                .define_fields                  = trace_event_define_fields_##call,                 \
                .fields                         = LIST_HEAD_INIT(event_class_##call.fields),\
                .raw_init                           = trace_event_raw_init,                             \
                .probe                              = trace_event_raw_event_##call,             \
                .reg                                    = trace_event_reg,                                      \
                _TRACE_PERF_INIT(call)                                                                          \
 };

#undef DEFINE_EVENT
#define DEFINE_EVENT(template, call, proto, args)                       \
                                                                        \
static struct trace_event_call __used event_##call = {                  \
        .class                  = &amp;event_class_##template,              \
        {                                                               \
                .tp                     = &amp;__tracepoint_##call,         \
        },                                                              \
        .event.funcs            = &amp;trace_event_type_funcs_##template,   \
        .print_fmt              = print_fmt_##template,                 \
        .flags                  = TRACE_EVENT_FL_TRACEPOINT,            \
};                                                                      \
static struct trace_event_call __used                                   \
__attribute__((section(&quot;_ftrace_events&quot;))) *__event_##call = &amp;event_##call

这一次宏展开,定义了一个trace_event_call的结构体。并且定义编译时将结构体存放在section("_ftrace_events"))段。当模块被加载时,内核会读取该段的数据,获取trace_event_call结构体。然后将trace_event_call注册到系统中。 下篇介绍,如何注册及使能。