<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
	<channel>
		<atom:link href="http://gentoo-zh.org/extern.php?action=feed&amp;fid=9&amp;type=rss" rel="self" type="application/rss+xml" />
		<title><![CDATA[Gentoo中文社区 / 内核模块]]></title>
		<link>http://www.gentoo-zh.org/index.php</link>
		<description><![CDATA[Gentoo中文社区 最近发表的主题。]]></description>
		<lastBuildDate>Sat, 13 Jul 2024 04:01:51 +0000</lastBuildDate>
		<generator>FluxBB</generator>
		<item>
			<title><![CDATA[Gentoo伯克利子系统之八:Linux tracing system对比分析]]></title>
			<link>http://www.gentoo-zh.org/viewtopic.php?id=874&amp;action=new</link>
			<description><![CDATA[<p>Linux trace技术发展已久，经常看到很多的专业术语，从perf LTTng systemtap&#160; bpftrace tracepoint&#160; trace BCC bpf ebpf等词汇，这些关键的词汇有着怎样的联系和关联，通过下面的这个图可以直观的认识到这几种关键技术的内在联系。</p><p>&#160; &#160; &#160; &#160; 整个内核的跟踪和观测技术分为前端工具，内核框架支持、内核数据源支持三个部分。前端工具通常作为使用者，在跟踪内核或者观测内核时使用的命令，比如：bpftrace BCC 等工具，内核态框架是指支持上述前端工具的内核框架，比如systemtap的内核模块、BPF的字节码等。内核态数据源是指对内核跟踪或者观测获取到的数据方法，比如kprobe和tracepoint等。</p><p><span class="postimg"><img src="https://www.batsom.net/usr/uploads/2024/05/3816854445.png" alt="FluxBB bbcode 测试" /></span></p><p>上述内核观测和跟踪技术的调用和对应关系如下：通过下面的关系图，可以看到清晰的调用关系。&#160; &#160; &#160;</p><p><span class="postimg"><img src="https://www.batsom.net/usr/uploads/2024/05/3411245156.png" alt="FluxBB bbcode 测试" /></span></p><p>前面的文章中介绍过Systemtap bpftrace perf在内核跟踪和观测方面的使用方法和实例。这里我们从几个维度对这几种技术进行对比分析。 </p><p><span class="postimg"><img src="https://www.batsom.net/usr/uploads/2024/05/2621854678.png" alt="FluxBB bbcode 测试" /></span></p><p>&#160; &#160; &#160; 从上面的几个维度的对比中我们能够更清晰的认识各类技术的特点。对于eBPF来说在各个方面都是占有优势的，</p><p>可编程性：BPF是可针对内核可编程的，</p><p>内核内置：也是现在内核中内置的技术框架，无需专门安装，只需要打开内核的配置参数。</p><p>安全性：BPF对编写的程序有安全检查</p><p>内核版本的支持范围：BPF主要针对高版本的内核，越高版本的内核，其包含的功能越丰富，对于老版本的内核不友好，但是systemtap基本通吃所有内核版本。</p><p>性能：在性能上这几种技术类型没有本质的区别</p><p>生态：BPF作为最近几年新发展的技术，围绕BPF的各类开源工具以及生态发展较快，其他几类工具基本处于发展停滞阶段。</p><p>功能丰富度：BPF除了内核的观测和跟踪方面发挥作用外，在内核安全、网络性能提升等多方向上都有对应的解决方案</p><p>使用便捷性：根据bpf技术的bpftrace bpf-tool等工具提供了大量可直接使用的工具，并且可以结合高级的编程语言python go进行开发，在使用便捷性上也有极大的优势。</p>]]></description>
			<author><![CDATA[dummy@example.com (batsom)]]></author>
			<pubDate>Sat, 13 Jul 2024 04:01:51 +0000</pubDate>
			<guid>http://www.gentoo-zh.org/viewtopic.php?id=874&amp;action=new</guid>
		</item>
		<item>
			<title><![CDATA[Gentoo伯克利子系统之七:静态跟踪点tracepoint]]></title>
			<link>http://www.gentoo-zh.org/viewtopic.php?id=873&amp;action=new</link>
			<description><![CDATA[<p>在《linux-observability-with-bpf》中第4章节中的案例中，有一个tracepoint bpf_prog_load的实例，在我自己的云主机上，执行不通过。发现网上也有一些人遇到。针对该问题分析了一下具体的原因。 根本原因是：内核版本的问题。<br />下面是《linux-observability-with-bpf》中的一个示例程序：</p><br /><div class="codebox"><pre><code>from bcc import BPF
 
bpf_source = &quot;&quot;&quot;
int trace_bpf_prog_load(struct pt_regs *ctx) {
  char comm[16];
  bpf_get_current_comm(&amp;comm, sizeof(comm));
  bpf_trace_printk(&quot;%s is loading a BPF program&quot;, comm);
  return 0;
}
&quot;&quot;&quot;
 
bpf = BPF(text = bpf_source)
bpf.attach_tracepoint(tp = &quot;bpf:bpf_prog_load&quot;, fn_name = &quot;trace_bpf_prog_load&quot;)
bpf.trace_print()</code></pre></div><p>上面的程序在bpf_prog_load的函数中添加一个添加一个tracepoint点。通过下面的命令可以参考本操作系统支持tracepoint的函数。<br />1）通过下面目录中，查看是否有相关的events</p><p><span class="postimg"><img src="https://www.batsom.net/usr/uploads/2024/05/2661943399.png" alt="FluxBB bbcode 测试" /></span></p><p>2）通过bcc的相关命令行工具</p><p><span class="postimg"><img src="https://www.batsom.net/usr/uploads/2024/05/980936701.png" alt="FluxBB bbcode 测试" /></span></p><p>&#160; &#160; &#160; &#160; 通过是上面的两个命令查看，都没有相关bpf的tracepoint点。所有执行上面的命令会出现下面的错误信息。open(/sys/kernel/debug/tracing/events/bpf/bpf_prog_load/id): No such file or directory </p><p><span class="postimg"><img src="https://www.batsom.net/usr/uploads/2024/05/3778500137.png" alt="FluxBB bbcode 测试" /></span></p><p>&#160; &#160; &#160; &#160; 通过对内核中相关的文件进行分析， 在kernel/bpf/syscall.c文件中，我们可以看到其中提交的一个commit，去掉对bpf_prog_load的tracepoint，使用git show 4d220ed 显示下面的信息</p><br /><p><span class="postimg"><img src="https://www.batsom.net/usr/uploads/2024/05/3104620357.png" alt="FluxBB bbcode 测试" /></span></p><p><span class="postimg"><img src="https://www.batsom.net/usr/uploads/2024/05/3104620357.png" alt="FluxBB bbcode 测试" /></span><br />&#160; &#160; &#160; &#160; 通过上面的查看，是在内核4.18之后的内核删除了。commit的描述是在有可能导致内核的panic。为了测试tracepoint的使用，可以使用下面的代码进行验证。 </p><div class="codebox"><pre><code>from bcc import BPF
 
bpf_source = &quot;&quot;&quot;
int trace_net_dev_xmit(struct pt_regs *ctx) {
  char comm[16];
  bpf_get_current_comm(&amp;comm, sizeof(comm));
  bpf_trace_printk(&quot;%s is loading a BPF program&quot;, comm);
  return 0;
}
&quot;&quot;&quot;
 
bpf = BPF(text = bpf_source)
bpf.attach_tracepoint(tp = &quot;net:net_dev_xmit&quot;, fn_name = &quot;trace_net_dev_xmit&quot;)
bpf.trace_print()</code></pre></div>]]></description>
			<author><![CDATA[dummy@example.com (batsom)]]></author>
			<pubDate>Sat, 13 Jul 2024 03:59:42 +0000</pubDate>
			<guid>http://www.gentoo-zh.org/viewtopic.php?id=873&amp;action=new</guid>
		</item>
		<item>
			<title><![CDATA[Gentoo伯克利子系统之六:使用bpf实现xdp的例子]]></title>
			<link>http://www.gentoo-zh.org/viewtopic.php?id=872&amp;action=new</link>
			<description><![CDATA[<p>本篇文章参考《Linux&#160; Observability with BPF》中第7章的例子，主要功能是借助于ip命令作为前端，对其他主机访问tcp的8000端口进行限制，这里需要使用较新版本的iproute2软件工具包. </p><p>1. 下载编译iproute2 工具包，使用最新的ip命令，支持配置xdp</p><p>&#160; &#160;git clone git://git.kernel.org/pub/scm/linux/kernel/git/shemminger/iproute2.git </p><p> 在编译iproute2时，需要开启支持libbpf的选项信息，在iproute2目录下配置使用下面的配置选项信息 </p><p>&#160; ./configure --libbpf_force=on --LIBBPF_DIR=/usr/local/lib64</p><p>&#160; &#160; &#160; &#160; 执行上面的命令，可能出现下面的错误信息，</p><p><span class="postimg"><img src="https://www.batsom.net/usr/uploads/2024/05/2842929636.png" alt="FluxBB bbcode 测试" /></span></p><p>确定libbpf的pk-config配置文件位置，/usr/local/lib64/pkgconfig/libbpf.pc&#160; 使用</p><p>export PKG_CONFIG_PATH=/usr/local/lib64/pkgconfig/ 导入libbpf的配置文件，</p><p>使用pkg-config --list-all | grep libbpf 查看是否配置libbpf</p><p><span class="postimg"><img src="https://www.batsom.net/usr/uploads/2024/05/1079572572.png" alt="FluxBB bbcode 测试" /></span></p><p>然后执行，make &amp;&amp; make install 进行安装。</p><p>2. 编写bpf程序</p><p>&#160; &#160; &#160; &#160;本bpf程序对于访问本机的tcp的协议的8000端口进行限制，</p><br /><div class="codebox"><pre class="vscroll"><code>#include &lt;linux/bpf.h&gt;
 
#include &lt;linux/if_ether.h&gt;
 
#include &lt;linux/in.h&gt;
 
#include &lt;linux/ip.h&gt;
 
#include &lt;linux/if_packet.h&gt;
 
#include &lt;bpf/bpf_helpers.h&gt;
 
#include &lt;linux/if_vlan.h&gt;
 
#include &lt;linux/types.h&gt;
 
#include &lt;linux/tcp.h&gt;
 
#include &lt;linux/udp.h&gt;
 
#include &lt;bpf/bpf_helpers.h&gt;
 
#include &lt;bpf/bpf_endian.h&gt;
 
 
 
static __always_inline int get_dport(void *trans_data, void *data_end, int protocol)
 
{
 
    struct tcphdr *th;
 
    struct udphdr *uh;
 
 
    switch (protocol) {
 
        case IPPROTO_TCP:
 
            th = (struct tcphdr *)trans_data;
 
            if ((void*)(th + 1) &gt; data_end)
 
                return -1;
 
            return th-&gt;dest;
 
        case IPPROTO_UDP:
 
            uh = (struct udphdr *)trans_data;
 
            if ((void *)(uh + 1) &gt; data_end)
 
                return -1;
 
            return uh-&gt;dest;
 
        default:
 
            return 0;
 
    }
 
 
}
 
 
SEC(&quot;mysection&quot;)
 
int myprogram(struct xdp_md *ctx) {
 
  void *data = (void *)(long)ctx-&gt;data;
 
  void *data_end = (void *)(long)ctx-&gt;data_end;
 
  char fmt[] = &quot;source = %d \n&quot;;
 
  struct iphdr *ip;
 
  int dport;
 
  int hdport;
 
  struct ethhdr *eth = data;
 
  struct iphdr *iph = data + sizeof(struct ethhdr);
 
 
    if ((void *)(iph + 1) &gt; data_end) {
 
        return XDP_DROP;
 
    }
 
 
  dport = get_dport(iph + 1, data_end,iph-&gt;protocol);
 
 
  if (dport == -1 || dport == bpf_htons(8000)) {
 
        bpf_trace_printk(fmt,sizeof(fmt),bpf_ntohs(dport));
 
      return XDP_DROP;
 
  }
 
 
  return XDP_PASS;
 
}
 
char _license [] SEC (&quot;license&quot;) = &quot;GPL&quot;;</code></pre></div><p>使用下面的命令进行编译：clang -g -c -O2 -target bpf -c program.c -o program.o</p><p>编译完成后，使用下面的ip命令对某个网卡进行价值xdp文件。</p><p>ip link set dev eth0 xdp obj program.o sec mysection&#160; &#160;</p><p>通过上面的命令加载后，在接口上出现加载的xdp的类型和ID，表明加载成功。</p><p><span class="postimg"><img src="https://www.batsom.net/usr/uploads/2024/05/859964227.png" alt="FluxBB bbcode 测试" /></span></p><p>使用python3 -m http.server在本地主机上其中http服务器，并监听8000端口。</p><p><span class="postimg"><img src="https://www.batsom.net/usr/uploads/2024/05/1179483150.png" alt="FluxBB bbcode 测试" /></span></p><p>使用另一台主机上使用nmap命令扫描对方监听的端口。nmap -sS 10.9.4.222，扫描结果如下：</p><p><span class="postimg"><img src="https://www.batsom.net/usr/uploads/2024/05/81153980.png" alt="FluxBB bbcode 测试" /></span></p><p>ip link set dev eth0 xdp off 关闭加载的xdp程序。再次使用nmap扫描</p>]]></description>
			<author><![CDATA[dummy@example.com (batsom)]]></author>
			<pubDate>Sat, 13 Jul 2024 03:55:52 +0000</pubDate>
			<guid>http://www.gentoo-zh.org/viewtopic.php?id=872&amp;action=new</guid>
		</item>
		<item>
			<title><![CDATA[Gentoo伯克利子系统之五:Systemtap BPF/BCC bpftrace 实践对比]]></title>
			<link>http://www.gentoo-zh.org/viewtopic.php?id=871&amp;action=new</link>
			<description><![CDATA[<p>本篇文章通过一个案例，对systemtap、BPF/BCC、bpftrace三种不同类型的内核探测工具进行剖析和对比。这个程序就是简单对icmp_rcv函数，收到icmp报文，打印出对应的源IP地址和目的IP地址。</p><p>1. 使用BPF/BCC<br />1.1在centos8操作系统上安装对应的软件二进制包</p><div class="codebox"><pre><code>1）	安装kernel-devel包；
2）	安装dnf -y install bcc-tools</code></pre></div><p>1.2 源码包安装</p><p>dnf install -y bison cmake ethtool flex git iperf3 libstdc+±devel python3-netaddr python3-pip gcc gcc-c++ make zlib-devel elfutils-libelf-devel<br />dnf install -y clang clang-devel llvm llvm-devel llvm-static ncurses-devel<br />dnf -y install netperf<br />pip3 install pyroute2<br />ln -s /usr/bin/python3 /usr/bin/python<br />dnf -y install openssl<br />git clone <a href="https://github.com/iovisor/bcc.git" rel="nofollow">https://github.com/iovisor/bcc.git</a><br />mkdir bcc_build<br />cmake …/bcc -DCMAKE_INSTALL_PREFIX=/usr -DENABLE_LLVM_SHARED=1<br />cd …/&amp;&amp; make -j10<br />make install<br />1.3 程序示例</p><p>使用bpf/bcc需要的内核版本最少是4.10以上。<br />使用下面的bcc代码，</p><br /><div class="codebox"><pre class="vscroll"><code>#!/usr/bin/env python3.6

from __future__ import print_function
from bcc import BPF
from bcc.utils import printb

bpf_text = &quot;&quot;&quot;
#include &lt;uapi/linux/ptrace.h&gt;
#include &lt;net/sock.h&gt;
#include &lt;bcc/proto.h&gt;
#include &lt;uapi/linux/icmp.h&gt;
#include &lt;linux/icmp.h&gt;
#include &lt;uapi/linux/ip.h&gt;
#include &lt;linux/ip.h&gt;


static inline struct iphdr *skb_to_iphdr(const struct sk_buff *skb)
{
    // unstable API. verify logic in ip_hdr() -&gt; skb_network_header().
    return (struct iphdr *)(skb-&gt;head + skb-&gt;network_header);
}

int icmp_rcv_cb(struct pt_regs *ctx, struct sk_buff *skb)
{
            struct icmphdr *icmph ;
            struct iphdr *iph = skb_to_iphdr(skb);
            bpf_trace_printk(&quot;ipsrc:%pI4  ipdst:%pI4 \\n&quot;,&amp;iph-&gt;saddr, &amp;iph-&gt;daddr);
            icmph = (struct icmphdr *)skb-&gt;data;
            bpf_trace_printk(&quot;devname:%s ----- icmp_type:%d  \\n&quot;,skb-&gt;dev-&gt;name, icmph-&gt;type);
            return 0;
};
&quot;&quot;&quot;
# initialize BPF
b = BPF(text=bpf_text)
b.attach_kprobe(event=&quot;icmp_rcv&quot;, fn_name=&quot;icmp_rcv_cb&quot;)
#end format output
while 1:
    # Read messages from kernel pipe
    (task, pid, cpu, flags, ts, msg) = b.trace_fields()
    print(&quot;task:%s pid: %d %s &quot; % (task, pid, msg))
#b.trace_print()</code></pre></div><p><span class="postimg"><img src="https://www.batsom.net/usr/uploads/2024/05/3230363645.png" alt="FluxBB bbcode 测试" /></span></p><p>2. Systemtap<br />2.1安装 systemtap</p><p>在centos8 上直接使用yum安装 yum install systemtap systemtap-runtime<br />2.2 Stap-prep</p><p><span class="postimg"><img src="https://www.batsom.net/usr/uploads/2024/05/1950940485.png" alt="FluxBB bbcode 测试" /></span></p><p>通过在http://debuginfo.centos.org/8/x86_64/Packages/下载安装完debuginfo包后，执行stap-prep命令</p><p><span class="postimg"><img src="https://www.batsom.net/usr/uploads/2024/05/736141600.png" alt="FluxBB bbcode 测试" /></span></p><p>简单测试可以运行成功</p><p><span class="postimg"><img src="https://www.batsom.net/usr/uploads/2024/05/4473753.png" alt="FluxBB bbcode 测试" /></span></p><p>2.3 程序示例</p><p>下面是systemtap的方式对icmp_rcv函数的探测，对本机收到的ICMP报文打印出，对应的源IP和目的IP地址。</p><br /><div class="codebox"><pre class="vscroll"><code>stap -g icmp_systemtap.stp
#!/usr/bin/stap -g
%{
#include &lt;linux/kernel.h&gt;
#include &lt;linux/net.h&gt;
#include &lt;linux/skbuff.h&gt;
#include &lt;net/ip.h&gt;
#include &lt;linux/module.h&gt;
#include &lt;uapi/linux/if_packet.h&gt;
#include &lt;linux/fdtable.h&gt;
#include &lt;net/icmp.h&gt;

        static inline void ip2str(char *to,unsigned int from)
        {
                int size = snprintf(to,16,&quot;%pI4&quot;,&amp;from);
                to[size] = &#039;\0&#039;;
         }
%}
function get_icmp_packet_info:string(skb:long)
%{
        int ret = -1;
        struct sk_buff *skb = (struct sk_buff *)STAP_ARG_skb;
        struct iphdr *ip_header;
        unsigned int src_ip_1 = 0;
        unsigned int  dst_ip_1 = 0;
        char src_ip[16],dst_ip[16];
        struct icmphdr *icmph;

        if(!skb)
        {
                goto EXIT_F;
        }

        ip_header = (struct iphdr *)skb_network_header(skb);


        if(!ip_header)
        {
                goto EXIT_F;
        }

        src_ip_1 = (unsigned int)ip_header-&gt;saddr;
        dst_ip_1 = (unsigned int)ip_header-&gt;daddr;
        ip2str(src_ip,src_ip_1);
        ip2str(dst_ip,dst_ip_1);

        icmph = icmp_hdr(skb);
        if(icmph-&gt;type == 0)
        {

                goto ECHO_ICMP;
        }
        if(icmph-&gt;type == 8)
        {
                goto REPLY_ICMP;
        }

EXIT_F:
        snprintf(STAP_RETVALUE,MAXSTRINGLEN,&quot;ERROR:src_ip:%s dst_ip:%s&quot;,src_ip,dst_ip);
ECHO_ICMP:
        snprintf(STAP_RETVALUE,MAXSTRINGLEN,&quot;ECHO_ICMP:src_ip:%s dst_ip:%s&quot;,src_ip,dst_ip);
REPLY_ICMP:
        snprintf(STAP_RETVALUE,MAXSTRINGLEN,&quot;REPLY_ICMP:src_ip:%s dst_ip:%s&quot;,src_ip,dst_ip);
%}

global locations

probe begin { printf(&quot;Monitoring for recv icmp packets\n&quot;) }
probe end { printf(&quot;Stropping monitoring  packets\n&quot;) }

probe kernel.function(&quot;icmp_rcv&quot;).return
{
        printf(&quot;%s\n&quot;,get_icmp_packet_info($skb))
        iphdr = __get_skb_iphdr($skb)
        saddr = format_ipaddr(__ip_skb_saddr(iphdr), @const(&quot;AF_INET&quot;))
        daddr = format_ipaddr(__ip_skb_daddr(iphdr), @const(&quot;AF_INET&quot;))
        printf(&quot;src_ip:%s  dst_ip:=%s\n&quot;,saddr,daddr);

}

probe timer.sec(5)
{
        exit ()
}</code></pre></div><p>下面是运行后的测试结果：</p><p><span class="postimg"><img src="https://www.batsom.net/usr/uploads/2024/05/3301897051.png" alt="FluxBB bbcode 测试" /></span></p><p>3. bpftrace<br />3.1 安装软件</p><p>yum -y install bpftrace<br />3.2 程序示例</p><p>bpftrace是使用自定义单行代码和简短脚本的临时工具的不错的选择，而BCC是复杂工具和守护程序的理想选择、bpftrace和BCC都是BPF的前端工具。</p><br /><div class="codebox"><pre class="vscroll"><code>在这里插入代码片#!/usr/bin/bpftrace

#include &lt;linux/skbuff.h&gt;
#include &lt;linux/ip.h&gt;
#include &lt;linux/udp.h&gt;
#include &lt;linux/socket.h&gt;

BEGIN
{
        printf(&quot;Tracing icmp rev.Hit  Ctrl-C end.\n&quot;);
}

kprobe:icmp_rcv
{
        $skb = (struct sk_buff *)arg0;

        $iph = (struct iphdr*)($skb-&gt;head + $skb-&gt;network_header);
        $src_ip = ntop(AF_INET,$iph-&gt;saddr);
        $dst_ip = ntop(AF_INET,$iph-&gt;daddr);

        printf(&quot;src_ip:%s  ----&gt; dst_ip:%s\n&quot;,$src_ip,$dst_ip);
}

END
{
        printf(&quot;OVER  bye!!&quot;)
}</code></pre></div><p>运行结果如下：</p><p><span class="postimg"><img src="https://www.batsom.net/usr/uploads/2024/05/2335592397.png" alt="FluxBB bbcode 测试" /></span></p><p>4 总结</p><p>&#160; &#160; 使用systemtap工具跟踪内核需要安装和内核对应版本的debuginfo包，systemtap作为老牌的内核跟踪工具，可以支持比较老的内核版本，对于现有存量的内核定位跟踪有明显的优势。<br />&#160; &#160; BPF/BCC作为新的内核跟踪工具，需要较新的内核版本，最少是4.10版本，最好是4.19版本的内核。<br />&#160; &#160; 通过运行对比发现，编译和运行BPF/BCC的代码比systemtap的代码要快的多。<br />&#160; &#160; BPF有各类安全检查，避免在内核跟踪过程中产生panic，systemtap没有此类的安全检查，需要开发者在开发systemtap程序时，保证代码的安全性。<br />&#160; &#160; Bpftrace作为内核跟踪的一种工具，特别适合简单的内核跟踪，适合一条命令搞定的内核跟踪，bpftrace也有自己的一套语法体系可用。<br />&#160; &#160; 各种不同类型的内核探测跟踪技术，适合不同类型的场景，在实际使用中可选择适合自己的方式。</p><p>参考文献：<br /><a href="https://lwn.net/Articles/852112/" rel="nofollow">https://lwn.net/Articles/852112/</a></p>]]></description>
			<author><![CDATA[dummy@example.com (batsom)]]></author>
			<pubDate>Sat, 13 Jul 2024 03:52:36 +0000</pubDate>
			<guid>http://www.gentoo-zh.org/viewtopic.php?id=871&amp;action=new</guid>
		</item>
		<item>
			<title><![CDATA[Gentoo伯克利子系统之四:使用sockmap示例：bypass内核协议栈]]></title>
			<link>http://www.gentoo-zh.org/viewtopic.php?id=870&amp;action=new</link>
			<description><![CDATA[<p>当我们使用127.0.0.1的localhost地址，在本地机器上进行发送和接受数据时，整个数据的发送还是要经过完整的内核协议栈。Cilimu的网络借用bfp的sockmap bypass提高整个网络的性能，参考《How to use eBPF for accelerating Cloud Native application》文章中的示例，在centos7.6平台上进行验证和测试。具体源码的github为：。</p><p>具体的原理解释性的内容就不从其他网页上搬迁过来了。主要有以下几点需要注意的问题，在这里记录以下：</p><p>1. 文章中使用的cgroup为cgroup v2版本的，在centos7上默认使用的是cgroup v1版本，可以通过修改内核的启动参数，添加对cgroup v2版本的支持，添加cgroup_no_v1=all 支持cgroup v2</p><p>2. 在添加cgroup v2支持后，使用mount命令看不到cgroup2的支持。使用下面的命令查看是否支持cgroup2，如果看到cgroup2 说明系统已经支持。</p><p><span class="postimg"><img src="https://www.batsom.net/usr/uploads/2024/05/1141627587.png" alt="FluxBB bbcode 测试" /></span></p><p>3. centos和ubuntu对cgroup v2的目录是不一样的，没有/sys/fs/cgroup/unified目录，需要自己创建，使用下面的命令创建cgroupv2的支持。</p><p>mkdir /root/cgroup2</p><p>mount -t cgroup2 nodev /root/cgroup2</p><p>4. 在attach的命令中，使用下面的cgroup2目录，bpftool cgroup attach &quot;/root/cgroup2/&quot; sock_ops pinned &quot;/sys/fs/bpf/bpf_sockop&quot;</p><p>测试结果：在一个窗口中开启：socat TCP4-LISTEN:1000,fork exec:cat作为服务器端，在另一个窗口中：nc localhost&#160; 1000。通过cat /sys/kernel/debug/tracing/trace_pipe文件可以看到端口号，通过在lo上面抓包也可以看到。</p><p><span class="postimg"><img src="https://www.batsom.net/usr/uploads/2024/05/1264818562.png" alt="FluxBB bbcode 测试" /></span></p><p>使用tcpmdump抓包</p><p>通过nc发送数据</p><p><span class="postimg"><img src="https://www.batsom.net/usr/uploads/2024/05/137316062.png" alt="FluxBB bbcode 测试" /></span></p><p><span class="postimg"><img src="https://www.batsom.net/usr/uploads/2024/05/313187464.png" alt="FluxBB bbcode 测试" /></span></p>]]></description>
			<author><![CDATA[dummy@example.com (batsom)]]></author>
			<pubDate>Sat, 13 Jul 2024 03:47:16 +0000</pubDate>
			<guid>http://www.gentoo-zh.org/viewtopic.php?id=870&amp;action=new</guid>
		</item>
		<item>
			<title><![CDATA[Gentoo伯克利子系统之三:BPF示例程序]]></title>
			<link>http://www.gentoo-zh.org/viewtopic.php?id=869&amp;action=new</link>
			<description><![CDATA[<p>上一篇文章中BPF的例子是以BCC的整体框架为基础，本篇介绍一下基于libbpf库函数为基础，结合内核中的bpf的sample为基础编写一个BPF的程序，本篇介绍是以《linux-observability-with-bpf》这本书第二章的例子为基础，由于内核版本的不同，本篇介绍是以Linux5.16内核为基础，Linux5.16内核中的接口函数与书中的给到的程序案例有较大的差别。<br />1. 下载并编译内核<br />1) 确定和编译内核版本<br />下载需要编译的内核版本，本次使用的内核版本为：Linux-5.16.11版本。<br />2) 修改内核的配置文件，设置CONFIG_DEBUG_INFO_BTF=y,编译调试，<br />3) 编译内核<br />make olddefconfig<br />make -j 4<br />make modules_install<br />make install<br />通过命令grub2-set-default 设置启动的内核<br />4) 重启机器使用安装的新内核版本：5.16.11.frank+<br />5) 确定/sys/kernel/btf/vmlinux文件是否存在。<br />2. 编译安装libbpf库<br />1） 进入目录tools/lib/bpf&#160; 在该目录下执行make install</p><p>2） 修改/etc/ld.so.conf 文件，添加/usr/local/lib64&#160; 执行ldconfig，查看ldconfig&#160; -v 2&gt; /dev/null | grep libbpf<br />如果没有编译libbpf库，在编译bpf程序中会出现，下面的错误信息<br />上述准备工作完毕后，有以下两种方式编译bpf的例子，第一种方式，把编写的bpf程序放到sample/bpf目录下，首先编译sample/bpf，<br />1. 编译内核下samples/bpf目录下的bpf<br />1） 在编译之前安装必要的工具：<br />yum -y install binutils-devel<br />yum -y install readline-devel<br />yum -y install&#160; dwarves&#160; libdwarves1&#160; libdwarves1-devel（dwarves版本号最好大于1.17）<br />yum -y install libcap-devel&#160; <br />2） 在sample/bpf目录下&#160; make <br />在编译的过程中，确定vmlinux的位置，<br />make VMLINUX_BTF=/sys/kernel/btf/vmlinux -C samples/bpf<br />使用vmlinux产生vmlinux.h头文件，CO:RE开发需要vmlinux.h文件，（Compile once, run everywhere）<br />bpftool btf dump file /sys/kernel/btf/vmlinux format c &gt; vmlinux.h<br />2. 编译自己编写的bpf程序<br />1)编译通过完成后，修改sample/bpf的目录下的Makefile文件，添加下面的三行代码：<br />hello-objs := hello_user.o<br />always-y += hello_kern.o<br />tprogs-y += hello<br />hello_user 为我们用户空间的程序名，hello_kern为我们的内核空间程序名。<br />2）Kernel hello_kern.c程序：</p><div class="codebox"><pre><code>#include &lt;linux/ptrace.h&gt;
#include &lt;linux/version.h&gt;
#include &lt;uapi/linux/bpf.h&gt;
#include &lt;bpf/bpf_helpers.h&gt;
#include &quot;trace_common.h&quot;
SEC(&quot;tracepoint/syscalls/sys_enter_execve&quot;)
 int bpf_prog(struct pt_regs *ctx) {
    char msg[] = &quot;Hello, BPF World!&quot;;
    bpf_trace_printk(msg, sizeof(msg));
    return 0;
  }
char _license[] SEC(&quot;license&quot;) = &quot;GPL&quot;;
   u32 _version SEC(&quot;version&quot;) = LINUX_VERSION_CODE;</code></pre></div><p>kernel程序比较简单，意思是在执行到内核中的execve函数时，打印 Hello BPF World！</p><p>3） 应用程序 hello_user.c</p><div class="codebox"><pre class="vscroll"><code>#include &lt;bpf/bpf.h&gt;
#include &lt;bpf/libbpf.h&gt;
#include &lt;fcntl.h&gt;
#include &lt;unistd.h&gt;
 
#define DEBUGFS &quot;/sys/kernel/debug/tracing/&quot;
int load_bpf_file(char *filename);
 
int load_bpf_file(char *path)
{
    struct bpf_object *obj;
    struct bpf_program *prog;
    struct bpf_link *link = NULL;
    int progs_fd;
    printf(&quot;%s\n&quot;,path);
 
    obj = bpf_object__open_file(path, NULL);
    if (libbpf_get_error(obj))
    {
        fprintf(stderr, &quot;ERROR: opening BPF object file failed\n&quot;);
        return 0;
    }
 
   if (bpf_object__load(obj))
    {
        fprintf(stderr, &quot;ERROR: loading BPF object file failed\n&quot;);
        goto cleanup;
    }
 
    prog = bpf_object__find_program_by_name(obj, &quot;bpf_prog&quot;);
    if (!prog) {
        printf(&quot;finding a prog in obj file failed\n&quot;);
        goto cleanup;
    }
 
    link = bpf_program__attach(prog);
    if (libbpf_get_error(link)) {
        fprintf(stderr, &quot;ERROR: bpf_program__attach failed\n&quot;);
        link = NULL;
        goto cleanup;
    }
 
 read_trace_pipe();
 
cleanup:
    bpf_link__destroy(link);
    bpf_object__close(obj);
    return 0;
}
 
void read_trace_pipe(void)
{
       int trace_fd;
 
       trace_fd = open(DEBUGFS &quot;trace_pipe&quot;, O_RDONLY, 0);
       if (trace_fd &lt; 0)
               return;
 
       while (1) {
               static char buf[4096];
               ssize_t sz;
 
               sz = read(trace_fd, buf, sizeof(buf) - 1);
               if (sz &gt; 0) {
                       buf[sz] = 0;
                       puts(buf);
               }
       }
}
 
int main(int argc, char **argv) {
   if (load_bpf_file(&quot;hello_kern.o&quot;) != 0) {
       printf(&quot;The kernel didn&#039;t load the BPF program\n&quot;);
       return -1;
   }
}</code></pre></div><p>执行上面的程序输出如下结果：<br /><span class="postimg"><img src="https://www.batsom.net/usr/uploads/2024/05/195980284.png" alt="FluxBB bbcode 测试" /></span></p><p>第二种方法:&#160; 如果不把编写的bpf示例程序放到，samples/bpf目录下，可以单独写一个makefile文件，内容如下：</p><div class="codebox"><pre class="vscroll"><code>CLANG = clang
 
EXECABLE = monitor-exec
 
BPFCODE = bpf_program
 
BPFTOOLS = /data/kernel/v1/linux-stable/samples/bpf
 
CCINCLUDE += -I/data/kernel/v1/linux-stable/tools/testing/selftests/bpf
 
LOADINCLUDE += -I/data/kernel/v1/linux-stable/samples/bpf
LOADINCLUDE += -I/data/kernel/v1/linux-stable//tools/lib
LOADINCLUDE += -I/data/kernel/v1/linux-stable/tools/perf
LOADINCLUDE += -I/data/kernel/v1/linux-stable/tools/include
LIBRARY_PATH = -L/usr/local/lib64
BPFSO = -lbpf
 
CFLAGS += $(shell grep -q &quot;define HAVE_ATTR_TEST 1&quot; /data/kernel/v1/linux-stable/tools/perf/perf-sys.h \
                 &amp;&amp; echo &quot;-DHAVE_ATTR_TEST=0&quot;)
 
.PHONY: clean $(CLANG) bpfload build
 
clean:
       rm -f *.o *.so $(EXECABLE)
 
build: ${BPFCODE.c}
       $(CLANG) -O2 -target bpf -c $(BPFCODE:=.c) $(CCINCLUDE) -o ${BPFCODE:=.o}
 
bpfload: build
       clang $(CFLAGS) -o $(EXECABLE) -lelf $(LOADINCLUDE) $(LIBRARY_PATH) $(BPFSO) \
       loader.c
 
$(EXECABLE): bpfload
 
.DEFAULT_GOAL := $(EXECABLE)</code></pre></div><p>1）本程序虽然以《linux-observability-with-bpf》第2章的程序为基础，但是随着内核的更新，采用5.16版本内核时load_bpf_file函数已经被移除了，需要重新调用函数实现load_bpf_file函数。<br />2）随着bpf和内核版本的不断变化，参考本文时需要重点关注不同的内核版本、bpftool、gcc等各类工具的版本。<br />3） 内核源代码中的samples/bpf目录下有大量的bpf的示例程序可以参考。<br />4）使用bcc框架版本的bpf程序和使用libbpf库bpf程序在编写方式上会有所不同，注意不同的接口函数。<br />5）centos安装libbpf -devel</p><br /><div class="codebox"><pre><code>sed -i -e &quot;s|mirrorlist=|#mirrorlist=|g&quot; /etc/yum.repos.d/CentOS-*

sed -i -e &quot;s|#baseurl=http://mirror.centos.org|baseurl=http://vault.centos.org|g&quot; /etc/yum.repos.d/CentOS-*

dnf --enablerepo=PowerTools install libbpf-devel</code></pre></div><br /><p>参考文献：https://blog.aquasec.com/vmlinux.h-ebpf-programs</p>]]></description>
			<author><![CDATA[dummy@example.com (batsom)]]></author>
			<pubDate>Sun, 23 Jun 2024 14:08:52 +0000</pubDate>
			<guid>http://www.gentoo-zh.org/viewtopic.php?id=869&amp;action=new</guid>
		</item>
		<item>
			<title><![CDATA[Gentoo伯克利子系统之二:基于BCC的BPF示例程序]]></title>
			<link>http://www.gentoo-zh.org/viewtopic.php?id=868&amp;action=new</link>
			<description><![CDATA[<p>结合前面一篇针对BPF的学习，本篇文章重点介绍编写一个对内核系统调用exec的例子。本测试例子基本上包含了全部的，syscall类别系统调用的BPF的框架。</p><br /><p>```python<br />#!/usr/bin/python<br />from __future__ import print_function<br />from bcc import BPF<br />from collections import defaultdict</p><p>bpf_text = &quot;&quot;&quot;<br />#include<br />#include<br />#include<br />#define ARGSIZE 256<br />struct data_t {<br />&#160; &#160; &#160; u32 pid; // PID as in the userspace term (i.e. task-&gt;tgid in kernel)<br />&#160; &#160; &#160; char comm[TASK_COMM_LEN];<br />&#160; &#160; &#160; char argv[ARGSIZE];<br /> };<br />BPF_PERF_OUTPUT(events);<br />int syscall__execve(struct pt_regs *ctx,<br />&#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160;const char __user *filename,<br />&#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160;const char __user *const __user *__argv,<br />&#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160;const char __user *const __user *__envp)<br />{<br />&#160; &#160; struct data_t data = {};<br />&#160; &#160; bpf_trace_printk(&quot;Hello, World!222%s\\n&quot;,filename);<br />&#160; &#160; data.pid = bpf_get_current_pid_tgid();<br />&#160; &#160; bpf_get_current_comm(&amp;data.comm,sizeof(data.comm));<br />&#160; &#160; bpf_probe_read_user(data.argv, sizeof(data.argv), filename);<br />&#160; &#160; events.perf_submit(ctx, &amp;data, sizeof(struct data_t));<br />&#160; &#160; return 0;<br />}<br />&quot;&quot;&quot;<br />b = BPF(text=bpf_text)<br />execve_fnname = b.get_syscall_fnname(&quot;execve&quot;)<br />b.attach_kprobe(event=execve_fnname, fn_name=&quot;syscall__execve&quot;)<br />print(&quot;%-18s %-16s %-14s&quot; % (&quot;COMM&quot;, &quot;PID&quot;,&quot;ARGS&quot;))</p><p> <br />argv = defaultdict(list)</p><p>def print_event(cpu, data, size):<br />&#160; &#160; event = b[&quot;events&quot;].event(data)<br />&#160; &#160; argv[event.pid].append(event.argv)</p><p>&#160; &#160; argv_text = b&#039; &#039;.join(argv[event.pid]).replace(b&#039;\n&#039;, b&#039;\\n&#039;)</p><p>&#160; &#160; print(&quot;%-18s %-16d %-14s&quot; % (event.comm, event.pid,argv_text))</p><p> <br />b[&quot;events&quot;].open_perf_buffer(print_event)<br />while 1:<br />&#160; &#160; try:<br />&#160; &#160; &#160; &#160; b.perf_buffer_poll()<br />&#160; &#160; except KeyboardInterrupt:<br />&#160; &#160; &#160; &#160; exit()<br />```</p><p>主要注意点：</p><p>1. BPF_PERF_OUTPUT&#160; &#160;创建BPF的table，通过Perf 的环形缓存区，把用户定义的event事件的数据推送到用户空间，这是把事件数据推送到用户空间的首选的方式。 也就是如果把从内核中获取到的数据，push到用户空间进行处理和展示，在代码中添加该宏。</p><p>2. b.attach_kprobe(event=execve_fnname, fn_name=&quot;syscall__execve&quot;) 这里的fn_name=&quot;syscal__execve&quot;函数名，保持格式的绝对一致，也就是syscall__ 后面两个下划线，execve名字和内核中syscall保持一致。<br />3.&#160; 在内核中插桩syscall__execve 函数时，这里的第一个参数struct pt_regs *ctx 保持固定，其余的参数是syscall类函数保持一致的，下面是调用execve的参数。</p><p> int execve(const char *pathname, char *const argv[], char *const envp[]);</p><p>4.&#160; 其实整个程序包含了两个部分，一个是python的客户端，一个是以字符串的形式bpf_text出现的插桩函数，使用C语言编写，整个测试程序包含了，用户空间和内核空间的代码。</p><p>5&#160; bpf_trace_printk函数，类似于C语言中printf，编写bpf程序时，可以用来调试打印相关信息。注意该打印函数会将信息输出到下面的文件中：/sys/kernel/debug/tracing/trace_pipe，通过跟踪该文件进行查看。</p><p>6. print_even 函数时对内核中获取的数据和信息的统一展示。</p>]]></description>
			<author><![CDATA[dummy@example.com (batsom)]]></author>
			<pubDate>Thu, 13 Jun 2024 13:31:35 +0000</pubDate>
			<guid>http://www.gentoo-zh.org/viewtopic.php?id=868&amp;action=new</guid>
		</item>
		<item>
			<title><![CDATA[Gentoo 伯克利子系统之一:BPF概述]]></title>
			<link>http://www.gentoo-zh.org/viewtopic.php?id=867&amp;action=new</link>
			<description><![CDATA[<p>BPF的英文是Berkeley&#160; Packet&#160; Filter的缩写，直白的翻译过来就是伯克利数据包过滤器，从这个英文翻译可以看出，BPF原先是针对网络的数据包进行各种操作处理的一个工具集。目前已经发展到对内核事件和用户空间事件进行跟踪的一套综合的工具集。BPF的原理简单解释为，在各种内核事件和应用程序事件的发生的前面，运行一小段程序，通过运行的这段程序获取到例如：函数调用的流程、结构体内的变量值等有利于分析问题的信息。BPF在2014年正式加入Linux内核主线，随着BPF的在内核中发展，以及 eBPF的出现，已经可以对内核中绝大部分的子系统进行跟踪观测。</p><p>&#160; &#160; &#160; &#160; BPF跟踪检测有两个前端的工具BCC和bpftrace。BCC为BPF编译器集合，BPF&#160; Compiler Collection，他提供了开发BPF跟踪程序的高级框架，提供了一个编写内核BPF程序的C语言环境，同时也提供了 Python和Lua等用户端的接口。BCC自身大概提供了97个BPF工具（截止到BCC-0.24版本。）,bpftrace大概提供了35个可用工具。当然我们可以在这个基础和框架之下，开发符合我们自己的实际需求的BPF工具。</p><p>&#160; &#160; &#160; &#160; &#160;BPF工具主要是为分析和定位Linux系统中出现一些性能问题，包括内核和应用程序两个方向，当然也可以通过打印出调用程序的堆栈信息，获取到函数的调用关系</p><p>BCC工具:<br /><span class="postimg"><img src="https://www.batsom.net/usr/uploads/2024/05/3511924330.png" alt="FluxBB bbcode 测试" /></span> </p><p>bpftrace工具:<br /><span class="postimg"><img src="https://www.batsom.net/usr/uploads/2024/05/1806307298.png" alt="FluxBB bbcode 测试" /></span> </p><br /><p>BPF主要思维导向图：<br /><span class="postimg"><img src="https://www.batsom.net/usr/uploads/2024/05/1733782732.png" alt="FluxBB bbcode 测试" /></span></p>]]></description>
			<author><![CDATA[dummy@example.com (batsom)]]></author>
			<pubDate>Wed, 12 Jun 2024 22:34:20 +0000</pubDate>
			<guid>http://www.gentoo-zh.org/viewtopic.php?id=867&amp;action=new</guid>
		</item>
		<item>
			<title><![CDATA[Gentoo时间子系统之八：动态时钟框架（CONFIG_NO_HZ、tickless）]]></title>
			<link>http://www.gentoo-zh.org/viewtopic.php?id=866&amp;action=new</link>
			<description><![CDATA[<p>在前面章节的讨论中，我们一直基于一个假设：Linux中的时钟事件都是由一个周期时钟提供，不管系统中的clock_event_device是工作于周期触发模式，还是工作于单触发模式，也不管定时器系统是工作于低分辨率模式，还是高精度模式，内核都竭尽所能，用不同的方式提供周期时钟，以产生定期的tick事件，tick事件或者用于全局的时间管理（jiffies和时间的更新），或者用于本地cpu的进程统计、时间轮定时器框架等等。周期性时钟虽然简单有效，但是也带来了一些缺点，尤其在系统的功耗上，因为就算系统目前无事可做，也必须定期地发出时钟事件，激活系统。为此，内核的开发者提出了动态时钟这一概念，我们可以通过内核的配置项CONFIG_NO_HZ来激活特性。有时候这一特性也被叫做tickless，不过还是把它称呼为动态时钟比较合适，因为并不是真的没有tick事件了，只是在系统无事所做的idle阶段，我们可以通过停止周期时钟来达到降低系统功耗的目的，只要有进程处于活动状态，时钟事件依然会被周期性地发出。</p><p>/*****************************************************************************************************/<br />声明：本博内容均由http://blog.csdn.net/droidphone原创，转载请注明出处，谢谢！<br />/*****************************************************************************************************/</p><p>在动态时钟正确工作之前，系统需要切换至动态时钟模式，而要切换至动态时钟模式，需要一些前提条件，最主要的一条就是cpu的时钟事件设备必须要支持单触发模式，当条件满足时，系统切换至动态时钟模式，接着，由idle进程决定是否可以停止周期时钟，退出idle进程时则需要恢复周期时钟。<br />1.&#160; 数据结构</p><p>在上一章的内容里，我们曾经提到，切换到高精度模式后，高精度定时器系统需要使用一个高精度定时器来模拟传统的周期时钟，其中利用了tick_sched结构中的一些字段，事实上，tick_sched结构也是实现动态时钟的一个重要的数据结构，在smp系统中，内核会为每个cpu都定义一个tick_sched结构，这通过一个percpu全局变量tick_cpu_sched来实现，它在kernel/time/tick-sched.c中定义：</p><div class="codebox"><pre><code>/*
 * Per cpu nohz control structure
 */
static DEFINE_PER_CPU(struct tick_sched, tick_cpu_sched);</code></pre></div><p>tick_sched结构在include/linux/tick.h中定义，我们看看tick_sched结构的详细定义： </p><div class="codebox"><pre><code>struct tick_sched {
	struct hrtimer			sched_timer;
	unsigned long			check_clocks;
	enum tick_nohz_mode		nohz_mode;
	ktime_t				idle_tick;
	int				inidle;
	int				tick_stopped;
	unsigned long			idle_jiffies;
	unsigned long			idle_calls;
	unsigned long			idle_sleeps;
	int				idle_active;
	ktime_t				idle_entrytime;
	ktime_t				idle_waketime;
	ktime_t				idle_exittime;
	ktime_t				idle_sleeptime;
	ktime_t				iowait_sleeptime;
	ktime_t				sleep_length;
	unsigned long			last_jiffies;
	unsigned long			next_jiffies;
	ktime_t				idle_expires;
	int				do_timer_last;
};</code></pre></div><p>sched_timer&#160; 该字段用于在高精度模式下，模拟周期时钟的一个hrtimer，请参看 Linux时间子系统之六：高精度定时器（HRTIMER）的原理和实现。</p><p>check_clocks&#160; 该字段用于实现clock_event_device和clocksource的异步通知机制，帮助系统切换至高精度模式或者是动态时钟模式。</p><p>nohz_mode&#160; 保存动态时钟的工作模式，基于低分辨率和高精度模式下，动态时钟的实现稍有不同，根据模式它可以是以下的值：</p><p>&gt;NOHZ_MODE_INACTIVE&#160; 系统动态时钟尚未激活<br />&gt;NOHZ_MODE_LOWRES&#160; 系统工作于低分辨率模式下的动态时钟<br />&gt;NOHZ_MODE_HIGHRES&#160; 系统工作于高精度模式下的动态时钟</p><p>idle_tick&#160; 该字段用于保存停止周期时钟是的内核时间，当退出idle时要恢复周期时钟，需要使用该时间，以保持系统中时间线（jiffies）的正确性。</p><p>tick_stopped&#160; 该字段用于表明idle状态的周期时钟已经停止。</p><p>idle_jiffies&#160; 系统进入idle时的jiffies值，用于信息统计。</p><p>idle_calls 系统进入idle的统计次数。</p><p>idle_sleeps&#160; 系统进入idle且成功停掉周期时钟的次数。</p><p>idle_active&#160; 表明目前系统是否处于idle状态中。</p><p>idle_entrytime&#160; 系统进入idle的时刻。</p><p>idle_waketime&#160; idle状态被打断的时刻。</p><p>idle_exittime&#160; 系统退出idle的时刻。</p><p>idle_sleeptime&#160; 累计各次idle中停止周期时钟的总时间。</p><p>sleep_length&#160; 本次idle中停止周期时钟的时间。</p><p>last_jiffies&#160; 系统中最后一次周期时钟的jiffies值。</p><p>next_jiffies&#160; 预计下一次周期时钟的jiffies。</p><p>idle_expires&#160; 进入idle后，下一个最先到期的定时器时刻。</p><p>我们知道，根据系统目前的工作模式，系统提供周期时钟（tick）的方式会有所不同，当处于低分辨率模式时，由cpu的tick_device提供周期时钟，而当处于高精度模式时，是由一个高精度定时器来提供周期时钟，下面我们分别讨论一下在两种模式下的动态时钟实现方式。<br />2.&#160; 低分辨率下的动态时钟<br />回看之前一篇文章： Linux时间子系统之四：定时器的引擎：clock_event_device中的关于tick_device一节，不管tick_device的工作模式（周期触发或者是单次触发），tick_device所关联的clock_event_device的事件回调处理函数都是：tick_handle_periodic，不管当前是否处于idle状态，他都会精确地按HZ数来提供周期性的tick事件，这不符合动态时钟的要求，所以，要使动态时钟发挥作用，系统首先要切换至支持动态时钟的工作模式：NOHZ_MODE_LOWRES&#160; 。</p><p>2.1&#160; 切换至动态时钟模式</p><p>动态时钟模式的切换过程的前半部分和切换至高精度定时器模式所经过的路径是一样的，请参考：Linux时间子系统之六：高精度定时器（HRTIMER）的原理和实现。这里再简单描述一下过程：系统工作于周期时钟模式，定期地发出tick事件中断，tick事件中断触发定时器软中断：TIMER_SOFTIRQ，执行软中断处理函数run_timer_softirq，run_timer_softirq调用hrtimer_run_pending函数：</p><div class="codebox"><pre><code>void hrtimer_run_pending(void)
{
	if (hrtimer_hres_active())
		return;
        ......
	if (tick_check_oneshot_change(!hrtimer_is_hres_enabled()))
		hrtimer_switch_to_hres();
}</code></pre></div><p>tick_check_oneshot_change函数的参数决定了现在是要切换至低分辨率动态时钟模式，还是高精度定时器模式，我们现在假设系统不支持高精度定时器模式，hrtimer_is_hres_enabled会直接返回false，对应的tick_check_oneshot_change函数的参数则是true，表明需要切换至动态时钟模式。tick_check_oneshot_change在检查过timekeeper和clock_event_device都具备动态时钟的条件后，通过tick_nohz_switch_to_nohz函数切换至动态时钟模式：</p><p>首先，该函数通过tick_switch_to_oneshot函数把tick_device的工作模式设置为单触发模式，并把它的中断事件回调函数置换为tick_nohz_handler，接着把tick_sched结构中的模式字段设置为NOHZ_MODE_LOWRES：</p><div class="codebox"><pre><code>static void tick_nohz_switch_to_nohz(void)
{
	struct tick_sched *ts = &amp;__get_cpu_var(tick_cpu_sched);
	ktime_t next;
 
	if (!tick_nohz_enabled)
		return;
 
	local_irq_disable();
	if (tick_switch_to_oneshot(tick_nohz_handler)) {
		local_irq_enable();
		return;
	}
 
	ts-&gt;nohz_mode = NOHZ_MODE_LOWRES;</code></pre></div><p>然后，初始化tick_sched结构中的sched_timer定时器，通过tick_init_jiffy_update获取下一次tick事件的时间并初始化全局变量last_jiffies_update，以便后续可以正确地更新jiffies计数值，最后，把下一次tick事件的时间编程到tick_device中，到此，系统完成了到低分辨率动态时钟的切换过程。 </p><div class="codebox"><pre><code>	hrtimer_init(&amp;ts-&gt;sched_timer, CLOCK_MONOTONIC, HRTIMER_MODE_ABS);
	/* Get the next period */
	next = tick_init_jiffy_update();
 
	for (;;) {
		hrtimer_set_expires(&amp;ts-&gt;sched_timer, next);
		if (!tick_program_event(next, 0))
			break;
		next = ktime_add(next, tick_period);
	}
	local_irq_enable();
}</code></pre></div><p>上面的代码中，明明现在没有切换至高精度模式，为什么要初始化tick_sched结构中的高精度定时器？原因并不是要使用它的定时功能，而是想重用hrtimer代码中的hrtimer_forward函数，利用这个函数来计算下一次tick事件的时间。</p><p>2.2&#160; 低分辨率动态时钟下的事件中断处理函数</p><p>上一节提到，当切换至低分辨率动态时钟模式后，tick_device的事件中断处理函数会被设置为tick_nohz_handler，总体来说，它和周期时钟模式的事件处理函数tick_handle_periodic所完成的工作大致类似：更新时间、更新jiffies计数值、调用update_process_time更新进程信息和触发定时器软中断等等，最后重新编程tick_device，使得它在下一个正确的tick时刻再次触发本函数：</p><div class="codebox"><pre><code>static void tick_nohz_handler(struct clock_event_device *dev)
{
        ......
	dev-&gt;next_event.tv64 = KTIME_MAX;
 
	if (unlikely(tick_do_timer_cpu == TICK_DO_TIMER_NONE))
		tick_do_timer_cpu = cpu;
 
	/* Check, if the jiffies need an update */
	if (tick_do_timer_cpu == cpu)
		tick_do_update_jiffies64(now);
        ......	
	if (ts-&gt;tick_stopped) {
		touch_softlockup_watchdog();
		ts-&gt;idle_jiffies++;
	}
 
	update_process_times(user_mode(regs));
	profile_tick(CPU_PROFILING);
 
	while (tick_nohz_reprogram(ts, now)) {
		now = ktime_get();
		tick_do_update_jiffies64(now);
	}
}</code></pre></div><p>因为现在工作于动态时钟模式，所以，tick时钟可能在idle进程中被停掉不止一个tick周期，所以当该函数被再次触发时，离上一次触发的时间可能已经不止一个tick周期，tick_nohz_reprogram对tick_device进行编程时必须正确地处理这一情况，它利用了前面所说的hrtimer_forward函数来实现这一特性： </p><div class="codebox"><pre><code>static int tick_nohz_reprogram(struct tick_sched *ts, ktime_t now)
{
	hrtimer_forward(&amp;ts-&gt;sched_timer, now, tick_period);
	return tick_program_event(hrtimer_get_expires(&amp;ts-&gt;sched_timer), 0);
}</code></pre></div><p>2.3&#160; 动态时钟：停止周期tick时钟事件</p><p>开启动态时钟模式后，周期时钟的开启和关闭由idle进程控制，idle进程内最终是一个循环，循环的一开始通过tick_nohz_idle_enter检测是否允许关闭周期时钟若干时间，然后进入低功耗的idle模式，当有中断事件使得cpu退出低功耗idle模式后，判断是否有新的进程被激活从而需要重新调度，如果需要则通过tick_nohz_idle_exit重新启用周期时钟，然后重新进行进程调度，等待下一次idle的发生，我们可以用下图来表示：</p><p><span class="postimg"><img src="https://www.batsom.net/usr/uploads/2024/04/2872503178.png" alt="FluxBB bbcode 测试" /></span></p><p>停止周期时钟的时机在tick_nohz_idle_enter函数中，它把主要的工作交由tick_nohz_stop_sched_tick函数来完成。内核也不是每次进入tick_nohz_stop_sched_tick都会停止周期时钟，那么什么时候才会停止？我们想一想，这时候既然idle进程在运行，说明系统中的其他进程都在等待某种事件，系统处于无事所做的状态，唯一要处理的就是中断，除了定时器中断，其它的中断我们无法预测它会何时发生，但是我们可以知道最先一个到期的定时器的到期时间，也就是说，在该时间到期前，产生周期时钟是没有必要的，我们可以据此推算出周期时钟可以停止的tick数，然后重新对tick_device进行编程，使得在最早一个定时器到期前都不会产生周期时钟，实际上，tick_nohz_stop_sched_tick还做了一些限制：当下一个定时器的到期时间与当前jiffies值只相差1时，不会停止周期时钟，当定时器的到期时间与当前的jiffies值相差的时间大于timekeeper允许的最大idle时间时，则下一个tick时刻被设置timekeeper允许的最大idle时间，这主要是为了防止太长时间不去更新timekeeper中的系统时间，有可能导致clocksource的溢出问题。tick_nohz_stop_sched_tick函数体看起来很长，实现的也就是上述的逻辑，所以这里就不贴它的代码了，有兴趣的读者可以自行阅读内核的代码：kernel/time/tick-sched.c。</p><p>看了动态时钟的停止过程和tick_nohz_handler的实现方式，其实还有一个情况没有处理：当系统进入idle进程后，周期时钟被停止若干个tick周期，当这若干个tick周期到期后，tick事件必然会产生，tick_nohz_handler被触发调用，然后最先到期的定时器被处理。但是在tick_nohz_handler的最后，tick_device一定会被编程为紧跟着的下一个tick周期的时刻被触发，如果刚才的定时器处理后，并没有激活新的进程，我们的期望是周期时钟可以用下一个新的定时器重新计算可以停止的时间，而不是下一个tick时刻，但是tick_nohz_handler却仅仅简单地把tick_device的到期时间设为下一个周期的tick时刻，这导致了周期时钟被恢复，显然这不是我们想要的。为了处理这种情况，内核使用了一点小伎俩，我们知道定时器是在软中断中执行的，所以内核在irq_exit中的软件中断处理完后，加入了一小段代码，kernel/softirq.c ：</p><div class="codebox"><pre><code>void irq_exit(void)
{
        ......
	if (!in_interrupt() &amp;&amp; local_softirq_pending())
		invoke_softirq();
 
#ifdef CONFIG_NO_HZ
	/* Make sure that timer wheel updates are propagated */
	if (idle_cpu(smp_processor_id()) &amp;&amp; !in_interrupt() &amp;&amp; !need_resched())
		tick_nohz_irq_exit();
#endif
        ......
}</code></pre></div><p>关键的调用是tick_nohz_irq_exit： </p><div class="codebox"><pre><code>void tick_nohz_irq_exit(void)
{
	struct tick_sched *ts = &amp;__get_cpu_var(tick_cpu_sched);
 
	if (!ts-&gt;inidle)
		return;
 
	tick_nohz_stop_sched_tick(ts);
}</code></pre></div><p>tick_nohz_irq_exit再次调用了tick_nohz_stop_sched_tick函数，使得系统有机会再次停止周期时钟若干个tick周期。<br />2.3&#160; 动态时钟：重新开启周期tick时钟事件</p><p>回到图2.3.1，当在idle进程中停止周期时钟后，在某一时刻，有新的进程被激活，在重新调度前，tick_nohz_idle_exit会被调用，该函数负责恢复被停止的周期时钟。tick_nohz_idle_exit最终会调用tick_nohz_restart函数，由tick_nohz_restart函数最后完成恢复周期时钟的工作。函数并不复杂：先是把上一次停止周期时钟的时刻设置到tick_sched结构的sched_timer定时器中，然后在通过hrtimer_forward函数把该定时器的到期时刻设置为当前时间的下一个tick时刻，对于高精度模式，启动该定时器即可，对于低分辨率模式，使用该时间对tick_device重新编程，最后通过tick_do_update_jiffies64更新jiffies数值，为了防止此时正在一个tick时刻的边界，可能当前时刻正好刚刚越过了该到期时间，函数使用了一个while循环：</p><div class="codebox"><pre><code>static void tick_nohz_restart(struct tick_sched *ts, ktime_t now)
{
	hrtimer_cancel(&amp;ts-&gt;sched_timer);
	hrtimer_set_expires(&amp;ts-&gt;sched_timer, ts-&gt;idle_tick);
 
	while (1) {
		/* Forward the time to expire in the future */
		hrtimer_forward(&amp;ts-&gt;sched_timer, now, tick_period);
 
		if (ts-&gt;nohz_mode == NOHZ_MODE_HIGHRES) {
			hrtimer_start_expires(&amp;ts-&gt;sched_timer,
					      HRTIMER_MODE_ABS_PINNED);
			/* Check, if the timer was already in the past */
			if (hrtimer_active(&amp;ts-&gt;sched_timer))
				break;
		} else {
			if (!tick_program_event(
				hrtimer_get_expires(&amp;ts-&gt;sched_timer), 0))
				break;
		}
		/* Reread time and update jiffies */
		now = ktime_get();
		tick_do_update_jiffies64(now);
	}
}</code></pre></div><p>3.&#160; 高精度模式下的动态时钟<br />高精度模式和低分辨率模式的主要区别是在切换过程中，怎样切换到高精度模式，我已经在上一篇文章中做了说明，切换到高精度模式后，动态时钟的开启和关闭和低分辨率模式下没有太大的区别，也是通过tick_nohz_stop_sched_tick和tick_nohz_restart来控制，在这两个函数中，分别判断了当前的两种模式：</p><p>&gt;NOHZ_MODE_HIGHRES<br />&gt;NOHZ_MODE_LOWRES</p><p>如果是NOHZ_MODE_HIGHRES则对tick_sched结构的sched_timer定时器进行设置，如果是NOHZ_MODE_LOWRES，则直接对tick_device进行操作。</p><p>4.&#160; 动态时钟对中断的影响<br />在进入和退出中断时，因为动态时钟的关系，中断系统需要作出一些配合。先说中断发生于周期时钟停止期间，如果不做任何处理，中断服务程序中如果要访问jiffies计数值，可能得到一个滞后的jiffies值，因为正常状态下，jiffies值会在恢复周期时钟时正确地更新，所以，为了防止这种情况发生，在进入中断的irq_enter期间，tick_check_idle会被调用： </p><div class="codebox"><pre><code>void tick_check_idle(int cpu)
{
	tick_check_oneshot_broadcast(cpu);
	tick_check_nohz(cpu);
}</code></pre></div><p>tick_check_nohz函数的最重要的作用就是更新jiffies计数值： </p><div class="codebox"><pre><code>static inline void tick_check_nohz(int cpu)
{
	struct tick_sched *ts = &amp;per_cpu(tick_cpu_sched, cpu);
	ktime_t now;
 
	if (!ts-&gt;idle_active &amp;&amp; !ts-&gt;tick_stopped)
		return;
	now = ktime_get();
	if (ts-&gt;idle_active)
		tick_nohz_stop_idle(cpu, now);
	if (ts-&gt;tick_stopped) {
		tick_nohz_update_jiffies(now);
		tick_nohz_kick_tick(cpu, now);
	}
}</code></pre></div><p>另外一种情况是在退出定时器中断时，需要重新评估周期时钟的运行状况，这一点已经在2.3节中做了说明，这里就不在赘述了。</p>]]></description>
			<author><![CDATA[dummy@example.com (batsom)]]></author>
			<pubDate>Tue, 23 Apr 2024 14:29:04 +0000</pubDate>
			<guid>http://www.gentoo-zh.org/viewtopic.php?id=866&amp;action=new</guid>
		</item>
		<item>
			<title><![CDATA[Gentoo时间子系统之七：定时器的应用--msleep()，hrtimer_nanosleep()]]></title>
			<link>http://www.gentoo-zh.org/viewtopic.php?id=865&amp;action=new</link>
			<description><![CDATA[<p>我们已经在前面几章介绍了低分辨率定时器和高精度定时器的实现原理，内核为了方便其它子系统，在时间子系统中提供了一些用于延时或调度的API，例如msleep，hrtimer_nanosleep等等，这些API基于低分辨率定时器或高精度定时器来实现，本章的内容就是讨论这些方便、好用的API是如何利用定时器系统来完成所需的功能的。</p><p>/*****************************************************************************************************/<br />声明：本博内容均由http://blog.csdn.net/droidphone原创，转载请注明出处，谢谢！<br />/*****************************************************************************************************/<br />1.&#160; msleep</p><p>msleep相信大家都用过，它可能是内核用使用最广泛的延时函数之一，它会使当前进程被调度并让出cpu一段时间，因为这一特性，它不能用于中断上下文，只能用于进程上下文中。要想在中断上下文中使用延时函数，请使用会阻塞cpu的无调度版本mdelay。msleep的函数原型如下：</p><p>&gt;void msleep(unsigned int msecs)</p><p>延时的时间由参数msecs指定，单位是毫秒，事实上，msleep的实现基于低分辨率定时器，所以msleep的实际精度只能也是1/HZ级别。内核还提供了另一个比较类似的延时函数msleep_interruptible：</p><p>unsigned long msleep_interruptible(unsigned int msecs)</p><p>延时的单位同样毫秒数，它们的区别如下： </p><p><span class="postimg"><img src="https://www.batsom.net/usr/uploads/2024/04/3128074307.png" alt="FluxBB bbcode 测试" /></span></p><p>最主要的区别就是msleep会保证所需的延时一定会被执行完，而msleep_interruptible则可以在延时进行到一半时被信号打断而退出延时，剩余的延时数则通过返回值返回。两个函数最终的代码都会到达schedule_timeout函数，它们的调用序列如下图所示： </p><p><span class="postimg"><img src="https://www.batsom.net/usr/uploads/2024/04/1825956184.png" alt="FluxBB bbcode 测试" /></span></p><p>下面我们看看schedule_timeout函数的实现，函数首先处理两种特殊情况，一种是传入的延时jiffies数是个负数，则打印一句警告信息，然后马上返回，另一种是延时jiffies数是MAX_SCHEDULE_TIMEOUT，表明需要一直延时，直接执行调度即可：</p><div class="codebox"><pre><code>signed long __sched schedule_timeout(signed long timeout)
{
	struct timer_list timer;
	unsigned long expire;
 
	switch (timeout)
	{
	case MAX_SCHEDULE_TIMEOUT:
		schedule();
		goto out;
	default:
		if (timeout &lt; 0) {
			printk(KERN_ERR &quot;schedule_timeout: wrong timeout &quot;
				&quot;value %lx\n&quot;, timeout);
			dump_stack();
			current-&gt;state = TASK_RUNNING;
			goto out;
		}
	}</code></pre></div><p>然后计算到期的jiffies数，并在堆栈上建立一个低分辨率定时器，把到期时间设置到该定时器中，启动定时器后，通过schedule把当前进程调度出cpu的运行队列：</p><div class="codebox"><pre><code>	expire = timeout + jiffies;
 
	setup_timer_on_stack(&amp;timer, process_timeout, (unsigned long)current);
	__mod_timer(&amp;timer, expire, false, TIMER_NOT_PINNED);
	schedule();</code></pre></div><p>到这个时候，进程已经被调度走，那它如何返回继续执行？我们看到定时器的到期回调函数是process_timeout，参数是当前进程的task_struct指针，看看它的实现： </p><div class="codebox"><pre><code>static void process_timeout(unsigned long __data)
{
	wake_up_process((struct task_struct *)__data);
}</code></pre></div><p>噢，没错，定时器一旦到期，进程会被唤醒并继续执行： </p><div class="codebox"><pre><code>	del_singleshot_timer_sync(&amp;timer);
 
	/* Remove the timer from the object tracker */
	destroy_timer_on_stack(&amp;timer);
 
	timeout = expire - jiffies;
 
 out:
	return timeout &lt; 0 ? 0 : timeout;
}</code></pre></div><p>schedule返回后，说明要不就是定时器到期，要不就是因为其它时间导致进程被唤醒，函数要做的就是删除在堆栈上建立的定时器，返回剩余未完成的jiffies数。</p><p>说完了关键的schedule_timeout函数，我们看看msleep如何实现：</p><div class="codebox"><pre><code>signed long __sched schedule_timeout_uninterruptible(signed long timeout)
{
	__set_current_state(TASK_UNINTERRUPTIBLE);
	return schedule_timeout(timeout);
}
 
void msleep(unsigned int msecs)
{
	unsigned long timeout = msecs_to_jiffies(msecs) + 1;
 
	while (timeout)
		timeout = schedule_timeout_uninterruptible(timeout);
}</code></pre></div><p>msleep先是把毫秒转换为jiffies数，通过一个while循环保证所有的延时被执行完毕，延时操作通过schedule_timeout_uninterruptible函数完成，它仅仅是在把进程的状态修改为TASK_UNINTERRUPTIBLE后，调用上述的schedule_timeout来完成具体的延时操作，TASK_UNINTERRUPTIBLE状态保证了msleep不会被信号唤醒，也就意味着在msleep期间，进程不能被kill掉。</p><p>看看msleep_interruptible的实现：</p><div class="codebox"><pre><code>signed long __sched schedule_timeout_interruptible(signed long timeout)
{
	__set_current_state(TASK_INTERRUPTIBLE);
	return schedule_timeout(timeout);
}
 
unsigned long msleep_interruptible(unsigned int msecs)
{
	unsigned long timeout = msecs_to_jiffies(msecs) + 1;
 
	while (timeout &amp;&amp; !signal_pending(current))
		timeout = schedule_timeout_interruptible(timeout);
	return jiffies_to_msecs(timeout);
}</code></pre></div><p>msleep_interruptible通过schedule_timeout_interruptible中转，schedule_timeout_interruptible的唯一区别就是把进程的状态设置为了TASK_INTERRUPTIBLE，说明在延时期间有信号通知，while循环会马上终止，剩余的jiffies数被转换成毫秒返回。实际上，你也可以利用schedule_timeout_interruptible或schedule_timeout_uninterruptible构造自己的延时函数，同时，内核还提供了另外一个类似的函数，不用我解释，看代码就知道它的用意了： </p><div class="codebox"><pre><code>signed long __sched schedule_timeout_killable(signed long timeout)
{
	__set_current_state(TASK_KILLABLE);
	return schedule_timeout(timeout);
}</code></pre></div><p>2.&#160; hrtimer_nanosleep<br />第一节讨论的msleep函数基于时间轮定时系统，只能提供毫秒级的精度，实际上，它的精度取决于HZ的配置值，如果HZ小于1000，它甚至无法达到毫秒级的精度，要想得到更为精确的延时，我们自然想到的是要利用高精度定时器来实现。没错，linux为用户空间提供了一个api：nanosleep，它能提供纳秒级的延时精度，该用户空间函数对应的内核实现是sys_nanosleep，它的工作交由高精度定时器系统的hrtimer_nanosleep函数实现，最终的大部分工作则由do_nanosleep完成。调用过程如下图所示： </p><p><span class="postimg"><img src="https://www.batsom.net/usr/uploads/2024/04/3468971015.png" alt="FluxBB bbcode 测试" /></span></p><p>与msleep的实现相类似，hrtimer_nanosleep函数首先在堆栈中创建一个高精度定时器，设置它的到期时间，然后通过do_nanosleep完成最终的延时工作，当前进程在挂起相应的延时时间后，退出do_nanosleep函数，销毁堆栈中的定时器并返回0值表示执行成功。不过do_nanosleep可能在没有达到所需延时数量时由于其它原因退出，如果出现这种情况，hrtimer_nanosleep的最后部分把剩余的延时时间记入进程的restart_block中，并返回ERESTART_RESTARTBLOCK错误代码，系统或者用户空间可以根据此返回值决定是否重新调用nanosleep以便把剩余的延时继续执行完成。下面是hrtimer_nanosleep的代码：</p><div class="codebox"><pre class="vscroll"><code>long hrtimer_nanosleep(struct timespec *rqtp, struct timespec __user *rmtp,
		       const enum hrtimer_mode mode, const clockid_t clockid)
{
	struct restart_block *restart;
	struct hrtimer_sleeper t;
	int ret = 0;
	unsigned long slack;
 
	slack = current-&gt;timer_slack_ns;
	if (rt_task(current))
		slack = 0;
 
	hrtimer_init_on_stack(&amp;t.timer, clockid, mode);
	hrtimer_set_expires_range_ns(&amp;t.timer, timespec_to_ktime(*rqtp), slack);
	if (do_nanosleep(&amp;t, mode))
		goto out;
 
	/* Absolute timers do not update the rmtp value and restart: */
	if (mode == HRTIMER_MODE_ABS) {
		ret = -ERESTARTNOHAND;
		goto out;
	}
 
	if (rmtp) {
		ret = update_rmtp(&amp;t.timer, rmtp);
		if (ret &lt;= 0)
			goto out;
	}
 
	restart = ¤t_thread_info()-&gt;restart_block;
	restart-&gt;fn = hrtimer_nanosleep_restart;
	restart-&gt;nanosleep.clockid = t.timer.base-&gt;clockid;
	restart-&gt;nanosleep.rmtp = rmtp;
	restart-&gt;nanosleep.expires = hrtimer_get_expires_tv64(&amp;t.timer);
 
	ret = -ERESTART_RESTARTBLOCK;
out:
	destroy_hrtimer_on_stack(&amp;t.timer);
	return ret;
}</code></pre></div><p>接着我们看看do_nanosleep的实现代码，它首先通过hrtimer_init_sleeper函数，把定时器的回调函数设置为hrtimer_wakeup，把当前进程的task_struct结构指针保存在hrtimer_sleeper结构的task字段中： </p><div class="codebox"><pre><code>void hrtimer_init_sleeper(struct hrtimer_sleeper *sl, struct task_struct *task)
{
	sl-&gt;timer.function = hrtimer_wakeup;
	sl-&gt;task = task;
}
EXPORT_SYMBOL_GPL(hrtimer_init_sleeper);
 
static int __sched do_nanosleep(struct hrtimer_sleeper *t, enum hrtimer_mode mode)
{
	hrtimer_init_sleeper(t, current);</code></pre></div><p>然后，通过一个do/while循环内：启动定时器，挂起当前进程，等待定时器或其它事件唤醒进程。这里的循环体实现比较怪异，它使用hrtimer_active函数间接地判断定时器是否到期，如果hrtimer_active返回false，说明定时器已经过期，然后把hrtimer_sleeper结构的task字段设置为NULL，从而导致循环体的结束，另一个结束条件是当前进程收到了信号事件，所以，当因为是定时器到期而退出时，do_nanosleep返回true，否则返回false，上述的hrtimer_nanosleep正是利用了这一特性来决定它的返回值。以下是do_nanosleep循环体的代码：</p><div class="codebox"><pre><code>	do {
		set_current_state(TASK_INTERRUPTIBLE);
		hrtimer_start_expires(&amp;t-&gt;timer, mode);
		if (!hrtimer_active(&amp;t-&gt;timer))
			t-&gt;task = NULL;
 
		if (likely(t-&gt;task))
			schedule();
 
		hrtimer_cancel(&amp;t-&gt;timer);
		mode = HRTIMER_MODE_ABS;
 
	} while (t-&gt;task &amp;&amp; !signal_pending(current));
 
	__set_current_state(TASK_RUNNING);
 
	return t-&gt;task == NULL;
}</code></pre></div><p>除了hrtimer_nanosleep，高精度定时器系统还提供了几种用于延时/挂起进程的api：</p><p>&#160; &#160; schedule_hrtimeout&#160; &#160; 使得当前进程休眠指定的时间，使用CLOCK_MONOTONIC计时系统；<br />&#160; &#160; schedule_hrtimeout_range&#160; &#160; 使得当前进程休眠指定的时间范围，使用CLOCK_MONOTONIC计时系统；<br />&#160; &#160; schedule_hrtimeout_range_clock&#160; &#160; 使得当前进程休眠指定的时间范围，可以自行指定计时系统；<br />&#160; &#160; usleep_range 使得当前进程休眠指定的微妙数，使用CLOCK_MONOTONIC计时系统；</p><p>它们之间的调用关系如下： </p><p><span class="postimg"><img src="https://www.batsom.net/usr/uploads/2024/04/1380680897.png" alt="FluxBB bbcode 测试" /></span></p><p>最终，所有的实现都会进入到schedule_hrtimeout_range_clock函数。需要注意的是schedule_hrtimeout_xxxx系列函数在调用前，最好利用set_current_state函数先设置进程的状态，在这些函数返回前，进城的状态会再次被设置为TASK_RUNNING。如果事先把状态设置为TASK_UNINTERRUPTIBLE，它们会保证函数返回前一定已经经过了所需的延时时间，如果事先把状态设置为TASK_INTERRUPTIBLE，则有可能在尚未到期时由其它信号唤醒进程从而导致函数返回。主要实现该功能的函数schedule_hrtimeout_range_clock和前面的do_nanosleep函数实现原理基本一致。大家可以自行参考内核的代码，它们位于：kernel/hrtimer.c。</p>]]></description>
			<author><![CDATA[dummy@example.com (batsom)]]></author>
			<pubDate>Tue, 23 Apr 2024 13:05:52 +0000</pubDate>
			<guid>http://www.gentoo-zh.org/viewtopic.php?id=865&amp;action=new</guid>
		</item>
		<item>
			<title><![CDATA[Gentoo时间子系统之六：高精度定时器（HRTIMER）的原理和实现]]></title>
			<link>http://www.gentoo-zh.org/viewtopic.php?id=864&amp;action=new</link>
			<description><![CDATA[<p>上一篇文章，我介绍了传统的低分辨率定时器的实现原理。而随着内核的不断演进，大牛们已经对这种低分辨率定时器的精度不再满足，而且，硬件也在不断地发展，系统中的定时器硬件的精度也越来越高，这也给高分辨率定时器的出现创造了条件。内核从2.6.16开始加入了高精度定时器架构。在实现方式上，内核的高分辨率定时器的实现代码几乎没有借用低分辨率定时器的数据结构和代码，内核文档给出的解释主要有以下几点：</p><p>&gt;低分辨率定时器的代码和jiffies的关系太过紧密，并且默认按32位进行设计，并且它的代码已经经过长时间的优化，目前的使用也是没有任何错误，如果硬要基于它来实现高分辨率定时器，势必会打破原有的时间轮概念，并且会引入一大堆#if--#else判断；<br />&gt;虽然大部分时间里，时间轮可以实现O(1)时间复杂度，但是当有进位发生时，不可预测的O(N)定时器级联迁移时间，这对于低分辨率定时器来说问题不大，可是它大大地影响了定时器的精度；<br />&gt;低分辨率定时器几乎是为“超时”而设计的，并为此对它进行了大量的优化，对于这些以“超时”未目的而使用定时器，它们大多数期望在超时到来之前获得正确的结果，然后删除定时器，精确时间并不是它们主要的目的，例如网络通信、设备IO等等。</p><p>为此，内核为高精度定时器重新设计了一套软件架构，它可以为我们提供纳秒级的定时精度，以满足对精确时间有迫切需求的应用程序或内核驱动，例如多媒体应用，音频设备的驱动程序等等。以下的讨论用hrtimer(high resolution timer)表示高精度定时器。<br />/*****************************************************************************************************/<br />声明：本博内容均由http://blog.csdn.net/droidphone原创，转载请注明出处，谢谢！<br />/*****************************************************************************************************/<br />1.&#160; 如何组织hrtimer？</p><p>我们知道，低分辨率定时器使用5个链表数组来组织timer_list结构，形成了著名的时间轮概念，对于高分辨率定时器，我们期望组织它们的数据结构至少具备以下条件：</p><p>&gt;稳定而且快速的查找能力；<br />&gt;快速地插入和删除定时器的能力；<br />&gt;排序功能；</p><p>内核的开发者考察了多种数据结构，例如基数树、哈希表等等，最终他们选择了红黑树（rbtree）来组织hrtimer，红黑树已经以库的形式存在于内核中，并被成功地使用在内存管理子系统和文件系统中，随着系统的运行，hrtimer不停地被创建和销毁，新的hrtimer按顺序被插入到红黑树中，树的最左边的节点就是最快到期的定时器，内核用一个hrtimer结构来表示一个高精度定时器：</p><div class="codebox"><pre><code>struct hrtimer {
	struct timerqueue_node		node;
	ktime_t				_softexpires;
	enum hrtimer_restart		(*function)(struct hrtimer *);
	struct hrtimer_clock_base	*base;
	unsigned long			state;
        ......
};</code></pre></div><p>定时器的到期时间用ktime_t来表示，_softexpires字段记录了时间，定时器一旦到期，function字段指定的回调函数会被调用，该函数的返回值为一个枚举值，它决定了该hrtimer是否需要被重新激活： </p><div class="codebox"><pre><code>enum hrtimer_restart {
	HRTIMER_NORESTART,	/* Timer is not restarted */
	HRTIMER_RESTART,	/* Timer must be restarted */
};</code></pre></div><p>state字段用于表示hrtimer当前的状态，有几下几种位组合： </p><div class="codebox"><pre><code>#define HRTIMER_STATE_INACTIVE	0x00  // 定时器未激活
#define HRTIMER_STATE_ENQUEUED	0x01  // 定时器已经被排入红黑树中
#define HRTIMER_STATE_CALLBACK	0x02  // 定时器的回调函数正在被调用
#define HRTIMER_STATE_MIGRATE	0x04  // 定时器正在CPU之间做迁移</code></pre></div><p>hrtimer的到期时间可以基于以下几种时间基准系统： </p><div class="codebox"><pre><code>enum  hrtimer_base_type {
	HRTIMER_BASE_MONOTONIC,  // 单调递增的monotonic时间，不包含休眠时间
	HRTIMER_BASE_REALTIME,   // 平常使用的墙上真实时间
	HRTIMER_BASE_BOOTTIME,   // 单调递增的boottime，包含休眠时间
	HRTIMER_MAX_CLOCK_BASES, // 用于后续数组的定义
};</code></pre></div><p>和低分辨率定时器一样，处于效率和上锁的考虑，每个cpu单独管理属于自己的hrtimer，为此，专门定义了一个结构hrtimer_cpu_base： </p><div class="codebox"><pre><code>struct hrtimer_cpu_base {
        ......
	struct hrtimer_clock_base	clock_base[HRTIMER_MAX_CLOCK_BASES];
};</code></pre></div><p>其中，clock_base数组为每种时间基准系统都定义了一个hrtimer_clock_base结构，它的定义如下： </p><div class="codebox"><pre><code>struct hrtimer_clock_base {
	struct hrtimer_cpu_base	*cpu_base;  // 指向所属cpu的hrtimer_cpu_base结构
        ......
	struct timerqueue_head	active;     // 红黑树，包含了所有使用该时间基准系统的hrtimer
	ktime_t			resolution; // 时间基准系统的分辨率
	ktime_t			(*get_time)(void); // 获取该基准系统的时间函数
	ktime_t			softirq_time;// 当用jiffies
	ktime_t			offset;      // 
};</code></pre></div><p>active字段是一个timerqueue_head结构，它实际上是对rbtree的进一步封装： </p><div class="codebox"><pre><code>struct timerqueue_node {
	struct rb_node node;  // 红黑树的节点
	ktime_t expires;      // 该节点代表队hrtimer的到期时间，与hrtimer结构中的_softexpires稍有不同
};
 
struct timerqueue_head {
	struct rb_root head;          // 红黑树的根节点
	struct timerqueue_node *next; // 该红黑树中最早到期的节点，也就是最左下的节点
};</code></pre></div><p>timerqueue_head结构在红黑树的基础上，增加了一个next字段，用于保存树中最先到期的定时器节点，实际上就是树的最左下方的节点，有了next字段，当到期事件到来时，系统不必遍历整个红黑树，只要取出next字段对应的节点进行处理即可。timerqueue_node用于表示一个hrtimer节点，它在标准红黑树节点rb_node的基础上增加了expires字段，该字段和hrtimer中的_softexpires字段一起，设定了hrtimer的到期时间的一个范围，hrtimer可以在hrtimer._softexpires至timerqueue_node.expires之间的任何时刻到期，我们也称timerqueue_node.expires为硬过期时间(hard)，意思很明显：到了此时刻，定时器一定会到期，有了这个范围可以选择，定时器系统可以让范围接近的多个定时器在同一时刻同时到期，这种设计可以降低进程频繁地被hrtimer进行唤醒。经过以上的讨论，我们可以得出以下的图示，它表明了每个cpu上的hrtimer是如何被组织在一起的： </p><p><span class="postimg"><img src="https://www.batsom.net/usr/uploads/2024/04/2199687912.png" alt="FluxBB bbcode 测试" /></span></p><p>总结一下：</p><p>&gt;每个cpu有一个hrtimer_cpu_base结构；<br />&gt;hrtimer_cpu_base结构管理着3种不同的时间基准系统的hrtimer，分别是：实时时间，启动时间和单调时间；<br />&gt;每种时间基准系统通过它的active字段（timerqueue_head结构指针），指向它们各自的红黑树；<br />&gt;红黑树上，按到期时间进行排序，最先到期的hrtimer位于最左下的节点，并被记录在active.next字段中；<br />&gt;3中时间基准的最先到期时间可能不同，所以，它们之中最先到期的时间被记录在hrtimer_cpu_base的expires_next字段中。</p><p>2.&#160; hrtimer如何运转</p><p>hrtimer的实现需要一定的硬件基础，它的实现依赖于我们前几章介绍的timekeeper和clock_event_device，如果你对timekeeper和clock_event_device不了解请参考以下文章：Linux时间子系统之三：时间的维护者：timekeeper，Linux时间子系统之四：定时器的引擎：clock_event_device。hrtimer系统需要通过timekeeper获取当前的时间，计算与到期时间的差值，并根据该差值，设定该cpu的tick_device（clock_event_device）的下一次的到期时间，时间一到，在clock_event_device的事件回调函数中处理到期的hrtimer。现在你或许有疑问：前面在介绍clock_event_device时，我们知道，每个cpu有自己的tick_device，通常用于周期性地产生进程调度和时间统计的tick事件，这里又说要用tick_device调度hrtimer系统，通常cpu只有一个tick_device，那他们如何协调工作？这个问题也一度困扰着我，如果再加上NO_HZ配置带来tickless特性，你可能会更晕。这里我们先把这个疑问放下，我将在后面的章节中来讨论这个问题，现在我们只要先知道，一旦开启了hrtimer，tick_device所关联的clock_event_device的事件回调函数会被修改为：hrtimer_interrupt，并且会被设置成工作于CLOCK_EVT_MODE_ONESHOT单触发模式。<br />2.1&#160; 添加一个hrtimer</p><p>要添加一个hrtimer，系统提供了一些api供我们使用，首先我们需要定义一个hrtimer结构的实例，然后用hrtimer_init函数对它进行初始化，它的原型如下：</p><div class="codebox"><pre><code>void hrtimer_init(struct hrtimer *timer, clockid_t which_clock,
			 enum hrtimer_mode mode);</code></pre></div><p>which_clock可以是CLOCK_REALTIME、CLOCK_MONOTONIC、CLOCK_BOOTTIME中的一种，mode则可以是相对时间HRTIMER_MODE_REL，也可以是绝对时间HRTIMER_MODE_ABS。设定回调函数： </p><div class="codebox"><pre><code>timer.function = hr_callback;</code></pre></div><p>如果定时器无需指定一个到期范围，可以在设定回调函数后直接使用hrtimer_start激活该定时器：</p><div class="codebox"><pre><code>int hrtimer_start(struct hrtimer *timer, ktime_t tim,
			 const enum hrtimer_mode mode);</code></pre></div><p>如果需要指定到期范围，则可以使用hrtimer_start_range_ns激活定时器： </p><div class="codebox"><pre><code>hrtimer_start_range_ns(struct hrtimer *timer, ktime_t tim,
			unsigned long range_ns, const enum hrtimer_mode mode);</code></pre></div><p>要取消一个hrtimer，使用hrtimer_cancel： </p><div class="codebox"><pre><code>int hrtimer_cancel(struct hrtimer *timer);</code></pre></div><p>以下两个函数用于推后定时器的到期时间： </p><div class="codebox"><pre><code>extern u64
hrtimer_forward(struct hrtimer *timer, ktime_t now, ktime_t interval);
 
/* Forward a hrtimer so it expires after the hrtimer&#039;s current now */
static inline u64 hrtimer_forward_now(struct hrtimer *timer,
				      ktime_t interval)
{
	return hrtimer_forward(timer, timer-&gt;base-&gt;get_time(), interval);
}</code></pre></div><p>以下几个函数用于获取定时器的当前状态： </p><div class="codebox"><pre><code>static inline int hrtimer_active(const struct hrtimer *timer)
{
	return timer-&gt;state != HRTIMER_STATE_INACTIVE;
}
 
static inline int hrtimer_is_queued(struct hrtimer *timer)
{
	return timer-&gt;state &amp; HRTIMER_STATE_ENQUEUED;
}
 
static inline int hrtimer_callback_running(struct hrtimer *timer)
{
	return timer-&gt;state &amp; HRTIMER_STATE_CALLBACK;
}</code></pre></div><p>hrtimer_init最终会进入__hrtimer_init函数，该函数的主要目的是初始化hrtimer的base字段，同时初始化作为红黑树的节点的node字段： </p><div class="codebox"><pre><code>static void __hrtimer_init(struct hrtimer *timer, clockid_t clock_id,
			   enum hrtimer_mode mode)
{
	struct hrtimer_cpu_base *cpu_base;
	int base;
 
	memset(timer, 0, sizeof(struct hrtimer));
 
	cpu_base = &amp;__raw_get_cpu_var(hrtimer_bases);
 
	if (clock_id == CLOCK_REALTIME &amp;&amp; mode != HRTIMER_MODE_ABS)
		clock_id = CLOCK_MONOTONIC;
 
	base = hrtimer_clockid_to_base(clock_id);
	timer-&gt;base = &amp;cpu_base-&gt;clock_base[base];
	timerqueue_init(&amp;timer-&gt;node);
        ......
}</code></pre></div><p>hrtimer_start和hrtimer_start_range_ns最终会把实际的工作交由__hrtimer_start_range_ns来完成：</p><div class="codebox"><pre class="vscroll"><code>int __hrtimer_start_range_ns(struct hrtimer *timer, ktime_t tim,
		unsigned long delta_ns, const enum hrtimer_mode mode,
		int wakeup)
{
        ......        
        /* 取得hrtimer_clock_base指针 */
        base = lock_hrtimer_base(timer, &amp;flags); 
        /* 如果已经在红黑树中，先移除它: */
        ret = remove_hrtimer(timer, base); ......
        /* 如果是相对时间，则需要加上当前时间，因为内部是使用绝对时间 */
        if (mode &amp; HRTIMER_MODE_REL) {
                tim = ktime_add_safe(tim, new_base-&gt;get_time());
                ......
        } 
        /* 设置到期的时间范围 */
        hrtimer_set_expires_range_ns(timer, tim, delta_ns);
        ...... 
        /* 把hrtime按到期时间排序，加入到对应时间基准系统的红黑树中 */
        /* 如果该定时器的是最早到期的，将会返回true */
        leftmost = enqueue_hrtimer(timer, new_base);
        /* 
        * Only allow reprogramming if the new base is on this CPU. 
        * (it might still be on another CPU if the timer was pending) 
        * 
        * XXX send_remote_softirq() ?
        * 定时器比之前的到期时间要早，所以需要重新对tick_device进行编程，重新设定的的到期时间
        */
        if (leftmost &amp;&amp; new_base-&gt;cpu_base == &amp;__get_cpu_var(hrtimer_bases))
                hrtimer_enqueue_reprogram(timer, new_base, wakeup);
        unlock_hrtimer_base(timer, &amp;flags);
        return ret;
}
 
 </code></pre></div><p>2.2&#160; hrtimer的到期处理</p><p>高精度定时器系统有3个入口可以对到期定时器进行处理，它们分别是：<br />&gt;没有切换到高精度模式时，在每个jiffie的tick事件中断中进行查询和处理；<br />&gt;在HRTIMER_SOFTIRQ软中断中进行查询和处理；<br />&gt;切换到高精度模式后，在每个clock_event_device的到期事件中断中进行查询和处理；</p><p>低精度模式&#160; 因为系统并不是一开始就会支持高精度模式，而是在系统启动后的某个阶段，等待所有的条件都满足后，才会切换到高精度模式，当系统还没有切换到高精度模式时，所有的高精度定时器运行在低精度模式下，在每个jiffie的tick事件中断中进行到期定时器的查询和处理，显然这时候的精度和低分辨率定时器是一样的（HZ级别）。低精度模式下，每个tick事件中断中，hrtimer_run_queues函数会被调用，由它完成定时器的到期处理。hrtimer_run_queues首先判断目前高精度模式是否已经启用，如果已经切换到了高精度模式，什么也不做，直接返回：</p><div class="codebox"><pre><code>void hrtimer_run_queues(void)
{
 
	if (hrtimer_hres_active())
		return;</code></pre></div><p>如果hrtimer_hres_active返回false，说明目前处于低精度模式下，则继续处理，它用一个for循环遍历各个时间基准系统，查询每个hrtimer_clock_base对应红黑树的左下节点，判断它的时间是否到期，如果到期，通过__run_hrtimer函数，对到期定时器进行处理，包括：调用定时器的回调函数、从红黑树中移除该定时器、根据回调函数的返回值决定是否重新启动该定时器等等：</p><div class="codebox"><pre><code>	for (index = 0; index &lt; HRTIMER_MAX_CLOCK_BASES; index++) {
		base = &amp;cpu_base-&gt;clock_base[index];
		if (!timerqueue_getnext(&amp;base-&gt;active))
			continue;
 
		if (gettime) {
			hrtimer_get_softirq_time(cpu_base);
			gettime = 0;
		}
 
		raw_spin_lock(&amp;cpu_base-&gt;lock);
 
		while ((node = timerqueue_getnext(&amp;base-&gt;active))) {
			struct hrtimer *timer;
 
			timer = container_of(node, struct hrtimer, node);
			if (base-&gt;softirq_time.tv64 &lt;=
					hrtimer_get_expires_tv64(timer))
				break;
 
			__run_hrtimer(timer, &amp;base-&gt;softirq_time);
		}
		raw_spin_unlock(&amp;cpu_base-&gt;lock);
	}</code></pre></div><p>上面的timerqueue_getnext函数返回红黑树中的左下节点，之所以可以在while循环中使用该函数，是因为__run_hrtimer会在移除旧的左下节点时，新的左下节点会被更新到base-&gt;active-&gt;next字段中，使得循环可以继续执行，直到没有新的到期定时器为止。</p><p>高精度模式&#160; 切换到高精度模式后，原来给cpu提供tick事件的tick_device（clock_event_device）会被高精度定时器系统接管，它的中断事件回调函数被设置为hrtimer_interrupt，红黑树中最左下的节点的定时器的到期时间被编程到该clock_event_device中，这样每次clock_event_device的中断意味着至少有一个高精度定时器到期。另外，当timekeeper系统中的时间需要修正，或者clock_event_device的到期事件时间被重新编程时，系统会发出HRTIMER_SOFTIRQ软中断，软中断的处理函数run_hrtimer_softirq最终也会调用hrtimer_interrupt函数对到期定时器进行处理，所以在这里我们只要讨论hrtimer_interrupt函数的实现即可。</p><p>hrtimer_interrupt函数的前半部分和低精度模式下的hrtimer_run_queues函数完成相同的事情：它用一个for循环遍历各个时间基准系统，查询每个hrtimer_clock_base对应红黑树的左下节点，判断它的时间是否到期，如果到期，通过__run_hrtimer函数，对到期定时器进行处理，所以我们只讨论后半部分，在处理完所有到期定时器后，下一个到期定时器的到期时间保存在变量expires_next中，接下来的工作就是把这个到期时间编程到tick_device中：</p><div class="codebox"><pre class="vscroll"><code>void hrtimer_interrupt(struct clock_event_device *dev)
{
        ......
	for (i = 0; i &lt; HRTIMER_MAX_CLOCK_BASES; i++) {
                ......
		while ((node = timerqueue_getnext(&amp;base-&gt;active))) {
                        ......
			if (basenow.tv64 &lt; hrtimer_get_softexpires_tv64(timer)) {
				ktime_t expires;
 
				expires = ktime_sub(hrtimer_get_expires(timer),
						    base-&gt;offset);
				if (expires.tv64 &lt; expires_next.tv64)
					expires_next = expires;
				break;
			}
 
			__run_hrtimer(timer, &amp;basenow);
		}
	}
 
	/*
	 * Store the new expiry value so the migration code can verify
	 * against it.
	 */
	cpu_base-&gt;expires_next = expires_next;
	raw_spin_unlock(&amp;cpu_base-&gt;lock);
 
	/* Reprogramming necessary ? */
	if (expires_next.tv64 == KTIME_MAX ||
	    !tick_program_event(expires_next, 0)) {
		cpu_base-&gt;hang_detected = 0;
		return;
	}</code></pre></div><p>如果这时的tick_program_event返回了非0值，表示过期时间已经在当前时间的前面，这通常由以下原因造成：</p><p>&gt;系统正在被调试跟踪，导致时间在走，程序不走；<br />&gt;定时器的回调函数花了太长的时间；<br />&gt;系统运行在虚拟机中，而虚拟机被调度导致停止运行；</p><p>为了避免这些情况的发生，接下来系统提供3次机会，重新执行前面的循环，处理到期的定时器： </p><div class="codebox"><pre><code>	raw_spin_lock(&amp;cpu_base-&gt;lock);
	now = hrtimer_update_base(cpu_base);
	cpu_base-&gt;nr_retries++;
	if (++retries &lt; 3)
		goto retry;</code></pre></div><p>如果3次循环后还无法完成到期处理，系统不再循环，转为计算本次总循环的时间，然后把tick_device的到期时间强制设置为当前时间加上本次的总循环时间，不过推后的时间被限制在100ms以内： </p><div class="codebox"><pre><code>	delta = ktime_sub(now, entry_time);
	if (delta.tv64 &gt; cpu_base-&gt;max_hang_time.tv64)
		cpu_base-&gt;max_hang_time = delta;
	/*
	 * Limit it to a sensible value as we enforce a longer
	 * delay. Give the CPU at least 100ms to catch up.
	 */
	if (delta.tv64 &gt; 100 * NSEC_PER_MSEC)
		expires_next = ktime_add_ns(now, 100 * NSEC_PER_MSEC);
	else
		expires_next = ktime_add(now, delta);
	tick_program_event(expires_next, 1);
	printk_once(KERN_WARNING &quot;hrtimer: interrupt took %llu ns\n&quot;,
		    ktime_to_ns(delta));
}</code></pre></div><p>3.&#160; 切换到高精度模式</p><p>上面提到，尽管内核配置成支持高精度定时器，但并不是一开始就工作于高精度模式，系统在启动的开始阶段，还是按照传统的模式在运行：tick_device按HZ频率定期地产生tick事件，这时的hrtimer工作在低分辨率模式，到期事件在每个tick事件中断中由hrtimer_run_queues函数处理，同时，在低分辨率定时器（时间轮）的软件中断TIMER_SOFTIRQ中，hrtimer_run_pending会被调用，系统在这个函数中判断系统的条件是否满足切换到高精度模式，如果条件满足，则会切换至高分辨率模式，另外提一下，NO_HZ模式也是在该函数中判断并切换。</p><div class="codebox"><pre><code>void hrtimer_run_pending(void)
{
	if (hrtimer_hres_active())
		return;
        ......
	if (tick_check_oneshot_change(!hrtimer_is_hres_enabled()))
		hrtimer_switch_to_hres();
}</code></pre></div><p>因为不管系统是否工作于高精度模式，每个TIMER_SOFTIRQ期间，该函数都会被调用，所以函数一开始先用hrtimer_hres_active判断目前高精度模式是否已经激活，如果已经激活，则说明之前的调用中已经切换了工作模式，不必再次切换，直接返回。hrtimer_hres_active很简单： </p><div class="codebox"><pre><code>DEFINE_PER_CPU(struct hrtimer_cpu_base, hrtimer_bases) = {
        ......
}
 
static inline int hrtimer_hres_active(void)
{
	return __this_cpu_read(hrtimer_bases.hres_active);
}</code></pre></div><p>hrtimer_run_pending函数接着通过tick_check_oneshot_change判断系统是否可以切换到高精度模式， </p><div class="codebox"><pre><code>int tick_check_oneshot_change(int allow_nohz)
{
	struct tick_sched *ts = &amp;__get_cpu_var(tick_cpu_sched);
 
	if (!test_and_clear_bit(0, &amp;ts-&gt;check_clocks))
		return 0;
 
	if (ts-&gt;nohz_mode != NOHZ_MODE_INACTIVE)
		return 0;
 
	if (!timekeeping_valid_for_hres() || !tick_is_oneshot_available())
		return 0;
 
	if (!allow_nohz)
		return 1;
 
	tick_nohz_switch_to_nohz();
	return 0;
}</code></pre></div><p>函数的一开始先判断check_clock标志的第0位是否被置位，如果没有置位，说明系统中没有注册符合要求的时钟事件设备，函数直接返回，check_clock标志由clocksource和clock_event_device系统的notify系统置位，当系统中有更高精度的clocksource被注册和选择后，或者有更精确的支持CLOCK_EVT_MODE_ONESHOT模式的clock_event_device被注册时，通过它们的notify函数，check_clock标志的第0为会置位。</p><p>如果tick_sched结构中的nohz_mode字段不是NOHZ_MODE_INACTIVE，表明系统已经切换到其它模式，直接返回。nohz_mode的取值有3种：<br />&gt;NOHZ_MODE_INACTIVE&#160; &#160; // 未启用NO_HZ模式<br />&gt;NOHZ_MODE_LOWRES&#160; &#160; // 启用NO_HZ模式，hrtimer工作于低精度模式下<br />&gt;NOHZ_MODE_HIGHRES&#160; &#160;// 启用NO_HZ模式，hrtimer工作于高精度模式下</p><p>接下来的timerkeeping_valid_for_hres判断timekeeper系统是否支持高精度模式，tick_is_oneshot_available判断tick_device是否支持CLOCK_EVT_MODE_ONESHOT模式。如果都满足要求，则继续往下判断。allow_nohz是函数的参数，为true表明可以切换到NOHZ_MODE_LOWRES 模式，函数将进入tick_nohz_switch_to_nohz，切换至NOHZ_MODE_LOWRES 模式，这里我们传入的allow_nohz是表达式：</p><p>&#160; &#160; (!hrtimer_is_hres_enabled())</p><p>所以当系统不允许高精度模式时，将会在tick_check_oneshot_change函数内，通过tick_nohz_switch_to_nohz切换至NOHZ_MODE_LOWRES 模式，如果系统允许高精度模式，传入的allow_nohz参数为false，tick_check_oneshot_change函数返回1，回到上面的hrtimer_run_pending函数，hrtimer_switch_to_hres函数将会被调用，已完成切换到NOHZ_MODE_HIGHRES高精度模式。好啦，真正的切换函数找到了，我们看一看它如何切换：</p><p>首先，它通过hrtimer_cpu_base中的hres_active字段判断该cpu是否已经切换至高精度模式，如果是则直接返回：</p><div class="codebox"><pre><code>static int hrtimer_switch_to_hres(void)
{
	int i, cpu = smp_processor_id();
	struct hrtimer_cpu_base *base = &amp;per_cpu(hrtimer_bases, cpu);
	unsigned long flags;
 
	if (base-&gt;hres_active)
		return 1;</code></pre></div><p>接着，通过tick_init_highres函数接管tick_device关联的clock_event_device： </p><div class="codebox"><pre><code>	local_irq_save(flags);
 
	if (tick_init_highres()) {
		local_irq_restore(flags);
		printk(KERN_WARNING &quot;Could not switch to high resolution &quot;
				    &quot;mode on CPU %d\n&quot;, cpu);
		return 0;
	}</code></pre></div><p>tick_init_highres函数把tick_device切换到CLOCK_EVT_FEAT_ONESHOT模式，同时把clock_event_device的回调handler设置为hrtimer_interrupt，这样设置以后，tick_device的中断回调将由hrtimer_interrupt接管，hrtimer_interrupt在上面已经讨论过，它将完成高精度定时器的调度和到期处理。</p><p>接着，设置hres_active标志，以表明高精度模式已经切换，然后把3个时间基准系统的resolution字段设为KTIME_HIGH_RES：</p><div class="codebox"><pre><code>	base-&gt;hres_active = 1;
	for (i = 0; i &lt; HRTIMER_MAX_CLOCK_BASES; i++)
		base-&gt;clock_base[i].resolution = KTIME_HIGH_RES;</code></pre></div><p>最后，因为tick_device被高精度定时器接管，它将不会再提供原有的tick事件机制，所以需要由高精度定时器系统模拟一个tick事件设备，继续为系统提供tick事件能力，这个工作由tick_setup_sched_timer函数完成。因为刚刚完成切换，tick_device的到期时间并没有被正确地设置为下一个到期定时器的时间，这里使用retrigger_next_event函数，传入参数NULL，使得tick_device立刻产生到期中断，hrtimer_interrupt被调用一次，然后下一个到期的定时器的时间会编程到tick_device中，从而完成了到高精度模式的切换： </p><div class="codebox"><pre><code>	tick_setup_sched_timer();
	/* &quot;Retrigger&quot; the interrupt to get things going */
	retrigger_next_event(NULL);
	local_irq_restore(flags);
	return 1;
}</code></pre></div><p>整个切换过程可以用下图表示：</p><p><span class="postimg"><img src="https://www.batsom.net/usr/uploads/2024/04/2937677647.png" alt="FluxBB bbcode 测试" /></span></p><p>4.&#160; 模拟tick事件</p><p>根据上一节的讨论，当系统切换到高精度模式后，tick_device被高精度定时器系统接管，不再定期地产生tick事件，我们知道，到目前的版本为止（V3.4），内核还没有彻底废除jiffies机制，系统还是依赖定期到来的tick事件，供进程调度系统和时间更新等操作，大量存在的低精度定时器也仍然依赖于jiffies的计数，所以，尽管tick_device被接管，高精度定时器系统还是要想办法继续提供定期的tick事件。为了达到这一目的，内核使用了一个取巧的办法：既然高精度模式已经启用，可以定义一个hrtimer，把它的到期时间设定为一个jiffy的时间，当这个hrtimer到期时，在这个hrtimer的到期回调函数中，进行和原来的tick_device同样的操作，然后把该hrtimer的到期时间顺延一个jiffy周期，如此反复循环，完美地模拟了原有tick_device的功能。下面我们看看具体点代码是如何实现的。</p><p>在kernel/time/tick-sched.c中，内核定义了一个per_cpu全局变量：tick_cpu_sched，从而为每个cpu提供了一个tick_sched结构， 该结构主要用于管理NO_HZ配置下的tickless处理，因为模拟tick事件与tickless有很强的相关性，所以高精度定时器系统也利用了该结构的以下字段，用于完成模拟tick事件的操作：</p><div class="codebox"><pre><code>struct tick_sched {
	struct hrtimer			sched_timer;
	unsigned long			check_clocks;
	enum tick_nohz_mode		nohz_mode;
        ......
};</code></pre></div><p>sched_timer就是要用于模拟tick事件的hrtimer，check_clock上面几节已经讨论过，用于notify系统通知hrtimer系统需要检查是否切换到高精度模式，nohz_mode则用于表示当前的工作模式。</p><p>上一节提到，用于切换至高精度模式的函数是hrtimer_switch_to_hres，在它的最后，调用了函数tick_setup_sched_timer，该函数的作用就是设置一个用于模拟tick事件的hrtimer：</p><div class="codebox"><pre class="vscroll"><code>void tick_setup_sched_timer(void)
{
	struct tick_sched *ts = &amp;__get_cpu_var(tick_cpu_sched);
	ktime_t now = ktime_get();
 
	/*
	 * Emulate tick processing via per-CPU hrtimers:
	 */
	hrtimer_init(&amp;ts-&gt;sched_timer, CLOCK_MONOTONIC, HRTIMER_MODE_ABS);
	ts-&gt;sched_timer.function = tick_sched_timer;
 
	/* Get the next period (per cpu) */
	hrtimer_set_expires(&amp;ts-&gt;sched_timer, tick_init_jiffy_update());
 
	for (;;) {
		hrtimer_forward(&amp;ts-&gt;sched_timer, now, tick_period);
		hrtimer_start_expires(&amp;ts-&gt;sched_timer,
				      HRTIMER_MODE_ABS_PINNED);
		/* Check, if the timer was already in the past */
		if (hrtimer_active(&amp;ts-&gt;sched_timer))
			break;
		now = ktime_get();
	}
 
#ifdef CONFIG_NO_HZ
	if (tick_nohz_enabled)
		ts-&gt;nohz_mode = NOHZ_MODE_HIGHRES;
#endif
}</code></pre></div><p>该函数首先初始化该cpu所属的tick_sched结构中sched_timer字段，把该hrtimer的回调函数设置为tick_sched_timer，然后把它的到期时间设定为下一个jiffy时刻，返回前把工作模式设置为NOHZ_MODE_HIGHRES，表明是利用高精度模式实现NO_HZ。</p><p>接着我们关注一下hrtimer的回调函数tick_sched_timer，我们知道，系统中的jiffies计数，时间更新等是全局操作，在smp系统中，只有一个cpu负责该工作，所以在tick_sched_timer的一开始，先判断当前cpu是否负责更新jiffies和时间，如果是，则执行更新操作：</p><div class="codebox"><pre><code>static enum hrtimer_restart tick_sched_timer(struct hrtimer *timer)
{
        ......
 
#ifdef CONFIG_NO_HZ
	if (unlikely(tick_do_timer_cpu == TICK_DO_TIMER_NONE))
		tick_do_timer_cpu = cpu;
#endif
 
	/* Check, if the jiffies need an update */
	if (tick_do_timer_cpu == cpu)
		tick_do_update_jiffies64(now);</code></pre></div><p>然后，利用regs指针确保当前是在中断上下文中，然后调用update_process_timer： </p><div class="codebox"><pre><code>	if (regs) {
                ......
		update_process_times(user_mode(regs));
		......
	}</code></pre></div><p>最后，把hrtimer的到期时间推进一个tick周期，返回HRTIMER_RESTART表明该hrtimer需要再次启动，以便产生下一个tick事件。</p><div class="codebox"><pre><code>	hrtimer_forward(timer, now, tick_period);
 
	return HRTIMER_RESTART;
}</code></pre></div><p>关于update_process_times，如果你你感兴趣，回看一下本系列关于clock_event_device的那一章： Linux时间子系统之四：定时器的引擎：clock_event_device中的第5小节，对比一下模拟tick事件的hrtimer的回调函数tick_sched_timer和切换前tick_device的回调函数 tick_handle_periodic，它们是如此地相像，实际上，它们几乎完成了一样的工作。</p>]]></description>
			<author><![CDATA[dummy@example.com (batsom)]]></author>
			<pubDate>Tue, 23 Apr 2024 12:55:19 +0000</pubDate>
			<guid>http://www.gentoo-zh.org/viewtopic.php?id=864&amp;action=new</guid>
		</item>
		<item>
			<title><![CDATA[Gentoo时间子系统之五：低分辨率定时器的原理和实现]]></title>
			<link>http://www.gentoo-zh.org/viewtopic.php?id=863&amp;action=new</link>
			<description><![CDATA[<p>利用定时器，我们可以设定在未来的某一时刻，触发一个特定的事件。所谓低分辨率定时器，是指这种定时器的计时单位基于jiffies值的计数，也就是说，它的精度只有1/HZ，假如你的内核配置的HZ是1000，那意味着系统中的低分辨率定时器的精度就是1ms。早期的内核版本中，内核并不支持高精度定时器，理所当然只能使用这种低分辨率定时器，我们有时候把这种基于HZ的定时器机制成为时间轮：time wheel。虽然后来出现了高分辨率定时器，但它只是内核的一个可选配置项，所以直到目前最新的内核版本，这种低分辨率定时器依然被大量地使用着。</p><p>/*****************************************************************************************************/<br />声明：本博内容均由http://blog.csdn.net/droidphone原创，转载请注明出处，谢谢！<br />/*****************************************************************************************************/</p><p>1.&#160; 定时器的使用方法</p><p>在讨论定时器的实现原理之前，我们先看看如何使用定时器。要在内核编程中使用定时器，首先我们要定义一个time_list结构，该结构在include/linux/timer.h中定义：</p><div class="codebox"><pre><code>struct timer_list {
	/*
	 * All fields that change during normal runtime grouped to the
	 * same cacheline
	 */
	struct list_head entry;
	unsigned long expires;
	struct tvec_base *base;
 
	void (*function)(unsigned long);
	unsigned long data;
 
	int slack;
        ......
};</code></pre></div><p>&gt;entry&#160; 字段用于把一组定时器组成一个链表，至于内核如何对定时器进行分组，我们会在后面进行解释。<br />&gt;expires&#160; 字段指出了该定时器的到期时刻，也就是期望定时器到期时刻的jiffies计数值。<br />&gt;base&#160; 每个cpu拥有一个自己的用于管理定时器的tvec_base结构，该字段指向该定时器所属的cpu所对应tvec_base结构。<br />&gt;function&#160; 字段是一个函数指针，定时器到期时，系统将会调用该回调函数，用于响应该定时器的到期事件。<br />&gt;data&#160; 该字段用于上述回调函数的参数。<br />&gt;slack&#160; 对有些对到期时间精度不太敏感的定时器，到期时刻允许适当地延迟一小段时间，该字段用于计算每次延迟的HZ数。</p><p>要定义一个timer_list，我们可以使用静态和动态两种办法，静态方法使用DEFINE_TIMER宏：<br /> &gt;#define DEFINE_TIMER(_name, _function, _expires, _data)</p><p>该宏将得到一个名字为_name，并分别用_function,_expires,_data参数填充timer_list的相关字段。</p><p>如果要使用动态的方法，则可以自己声明一个timer_list结构，然后手动初始化它的各个字段：</p><div class="codebox"><pre><code>struct timer_list timer;
......
init_timer(&amp;timer);
timer.function = _function;
timer.expires = _expires;
timer.data = _data;</code></pre></div><p>要激活一个定时器，我们只要调用add_timer即可：</p><p> add_timer(&amp;timer);</p><p>要修改定时器的到期时间，我们只要调用mod_timer即可：</p><p> mod_timer(&amp;timer, jiffies+50);</p><p>要移除一个定时器，我们只要调用del_timer即可：</p><p> del_timer(&amp;timer);</p><p>定时器系统还提供了以下这些API供我们使用：</p><p>&gt;void add_timer_on(struct timer_list *timer, int cpu);&#160; // 在指定的cpu上添加定时器<br />&gt;int mod_timer_pending(struct timer_list *timer, unsigned long expires);&#160; //&#160; 只有当timer已经处在激活状态时，才修改timer的到期时刻<br />&gt;int mod_timer_pinned(struct timer_list *timer, unsigned long expires);&#160; //&#160; 当<br />&gt;void set_timer_slack(struct timer_list *time, int slack_hz);&#160; //&#160; 设定timer允许的到期时刻的最大延迟，用于对精度不敏感的定时器<br />&gt;int del_timer_sync(struct timer_list *timer);&#160; //&#160; 如果该timer正在被处理中，则等待timer处理完成才移除该timer</p><p>2.&#160; 定时器的软件架构</p><p>低分辨率定时器是基于HZ来实现的，也就是说，每个tick周期，都有可能有定时器到期，关于tick如何产生，请参考：Linux时间子系统之四：定时器的引擎：clock_event_device。系统中有可能有成百上千个定时器，难道在每个tick中断中遍历一下所有的定时器，检查它们是否到期？内核当然不会使用这么笨的办法，它使用了一个更聪明的办法：按定时器的到期时间对定时器进行分组。因为目前的多核处理器使用越来越广泛，连智能手机的处理器动不动就是4核心，内核对多核处理器有较好的支持，低分辨率定时器在实现时也充分地考虑了多核处理器的支持和优化。为了较好地利用cache line，也为了避免cpu之间的互锁，内核为多核处理器中的每个cpu单独分配了管理定时器的相关数据结构和资源，每个cpu独立地管理属于自己的定时器。<br />2.1&#160; 定时器的分组</p><p>首先，内核为每个cpu定义了一个tvec_base结构指针：<br /> static DEFINE_PER_CPU(struct tvec_base *, tvec_bases) = &amp;boot_tvec_bases;</p><p>tvec_base结构的定义如下： </p><div class="codebox"><pre><code>struct tvec_base {
	spinlock_t lock;
	struct timer_list *running_timer;
	unsigned long timer_jiffies;
	unsigned long next_timer;
	struct tvec_root tv1;
	struct tvec tv2;
	struct tvec tv3;
	struct tvec tv4;
	struct tvec tv5;
} ____cacheline_aligned;</code></pre></div><p>running_timer&#160; 该字段指向当前cpu正在处理的定时器所对应的timer_list结构。</p><p>timer_jiffies&#160; 该字段表示当前cpu定时器所经历过的jiffies数，大多数情况下，该值和jiffies计数值相等，当cpu的idle状态连续持续了多个jiffies时间时，当退出idle状态时，jiffies计数值就会大于该字段，在接下来的tick中断后，定时器系统会让该字段的值追赶上jiffies值。</p><p>next_timer&#160; 该字段指向该cpu下一个即将到期的定时器。</p><p>tv1--tv5&#160; 这5个字段用于对定时器进行分组，实际上，tv1--tv5都是一个链表数组，其中tv1的数组大小为TVR_SIZE， tv2 tv3 tv4 tv5的数组大小为TVN_SIZE，根据CONFIG_BASE_SMALL配置项的不同，它们有不同的大小：</p><div class="codebox"><pre><code>#define TVN_BITS (CONFIG_BASE_SMALL ? 4 : 6)
#define TVR_BITS (CONFIG_BASE_SMALL ? 6 : 8)
#define TVN_SIZE (1 &lt;&lt; TVN_BITS)
#define TVR_SIZE (1 &lt;&lt; TVR_BITS)
#define TVN_MASK (TVN_SIZE - 1)
#define TVR_MASK (TVR_SIZE - 1)
 
struct tvec {
	struct list_head vec[TVN_SIZE];
};
 
struct tvec_root {
	struct list_head vec[TVR_SIZE];
};</code></pre></div><p>默认情况下，没有使能CONFIG_BASE_SMALL，TVR_SIZE的大小是256，TVN_SIZE的大小则是64，当需要节省内存空间时，也可以使能CONFIG_BASE_SMALL，这时TVR_SIZE的大小是64，TVN_SIZE的大小则是16，以下的讨论我都是基于没有使能CONFIG_BASE_SMALL的情况。当有一个新的定时器要加入时，系统根据定时器到期的jiffies值和timer_jiffies字段的差值来决定该定时器被放入tv1至tv5中的哪一个数组中，最终，系统中所有的定时器的组织结构如下图所示： </p><p><span class="postimg"><img src="https://www.batsom.net/usr/uploads/2024/04/4151975474.png" alt="FluxBB bbcode 测试" /></span></p><p>2.2&#160; 定时器的添加</p><p>要加入一个新的定时器，我们可以通过api函数add_timer或mod_timer来完成，最终的工作会交由internal_add_timer函数来处理。该函数按以下步骤进行处理：</p><p>&gt;计算定时器到期时间和所属cpu的tvec_base结构中的timer_jiffies字段的差值，记为idx；<br />&gt;根据idx的值，选择该定时器应该被放到tv1--tv5中的哪一个链表数组中，可以认为tv1-tv5分别占据一个32位数的不同比特位，tv1占据最低的8位，tv2占据紧接着的6位，然后tv3再占位，以此类推，最高的6位分配给tv5。最终的选择规则如下表所示：</p><p><span class="postimg"><img src="https://www.batsom.net/usr/uploads/2024/04/1813907493.png" alt="FluxBB bbcode 测试" /></span></p><p>确定链表数组后，接着要确定把该定时器放入数组中的哪一个链表中，如果时间差idx小于256，按规则要放入tv1中，因为tv1包含了256个链表，所以可以简单地使用timer_list.expires的低8位作为数组的索引下标，把定时器链接到tv1中相应的链表中即可。如果时间差idx的值在256--18383之间，则需要把定时器放入tv2中，同样的，使用timer_list.expires的8--14位作为数组的索引下标，把定时器链接到tv2中相应的链表中,。定时器要加入tv3 tv4 tv5使用同样的原理。经过这样分组后的定时器，在后续的tick事件中，系统可以很方便地定位并取出相应的到期定时器进行处理。以上的讨论都体现在internal_add_timer的代码中：</p><div class="codebox"><pre><code>static void internal_add_timer(struct tvec_base *base, struct timer_list *timer)
{
	unsigned long expires = timer-&gt;expires;
	unsigned long idx = expires - base-&gt;timer_jiffies;
	struct list_head *vec;
 
	if (idx &lt; TVR_SIZE) {
		int i = expires &amp; TVR_MASK;
		vec = base-&gt;tv1.vec + i;
	} else if (idx &lt; 1 &lt;&lt; (TVR_BITS + TVN_BITS)) {
		int i = (expires &gt;&gt; TVR_BITS) &amp; TVN_MASK;
		vec = base-&gt;tv2.vec + i;
	} else if (idx &lt; 1 &lt;&lt; (TVR_BITS + 2 * TVN_BITS)) {
		int i = (expires &gt;&gt; (TVR_BITS + TVN_BITS)) &amp; TVN_MASK;
		vec = base-&gt;tv3.vec + i;
	} else if (idx &lt; 1 &lt;&lt; (TVR_BITS + 3 * TVN_BITS)) {
		int i = (expires &gt;&gt; (TVR_BITS + 2 * TVN_BITS)) &amp; TVN_MASK;
		vec = base-&gt;tv4.vec + i;
	} else if ((signed long) idx &lt; 0) {
                ......
	} else {
                ......
		i = (expires &gt;&gt; (TVR_BITS + 3 * TVN_BITS)) &amp; TVN_MASK;
		vec = base-&gt;tv5.vec + i;
	}
	list_add_tail(&amp;timer-&gt;entry, vec);
}</code></pre></div><p>2.2&#160; 定时器的到期处理</p><p>经过2.1节的处理后，系统中的定时器按到期时间有规律地放置在tv1--tv5各个链表数组中，其中tv1中放置着在接下来的256个jiffies即将到期的定时器列表，需要注意的是，并不是tv1.vec[0]中放置着马上到期的定时器列表，tv1.vec[1]中放置着将在jiffies+1到期的定时器列表。因为base.timer_jiffies的值一直在随着系统的运行而动态地增加，原则上是每个tick事件会加1，base.timer_jiffies代表者该cpu定时器系统当前时刻，定时器也是动态地加入头256个链表tv1中，按2.1节的讨论，定时器加入tv1中使用的下标索引是定时器到期时间expires的低8位，所以假设当前的base.timer_jiffies值是0x34567826，则马上到期的定时器是在tv1.vec[0x26]中，如果这时候系统加入一个在jiffies值0x34567828到期的定时器，他将会加入到tv1.vec[0x28]中，运行两个tick后，base.timer_jiffies的值会变为0x34567828，很显然，在每次tick事件中，定时器系统只要以base.timer_jiffies的低8位作为索引，取出tv1中相应的链表，里面正好包含了所有在该jiffies值到期的定时器列表。</p><p>那什么时候处理tv2--tv5中的定时器？每当base.timer_jiffies的低8位为0值时，这表明base.timer_jiffies的第8-13位有进位发生，这6位正好代表着tv2，这时只要按base.timer_jiffies的第8-13位的值作为下标，移出tv2中对应的定时器链表，然后用internal_add_timer把它们从新加入到定时器系统中来，因为这些定时器一定会在接下来的256个tick期间到期，所以它们肯定会被加入到tv1数组中，这样就完成了tv2往tv1迁移的过程。同样地，当base.timer_jiffies的第8-13位为0时，这表明base.timer_jiffies的第14-19位有进位发生，这6位正好代表着tv3，按base.timer_jiffies的第14-19位的值作为下标，移出tv3中对应的定时器链表，然后用internal_add_timer把它们从新加入到定时器系统中来，显然它们会被加入到tv2中，从而完成tv3到tv2的迁移，tv4，tv5的处理可以以此作类推。具体迁移的代码如下，参数index为事先计算好的高一级tv的需要迁移的数组索引：</p><div class="codebox"><pre><code>static int cascade(struct tvec_base *base, struct tvec *tv, int index)
{
	/* cascade all the timers from tv up one level */
	struct timer_list *timer, *tmp;
	struct list_head tv_list;
 
	list_replace_init(tv-&gt;vec + index, &amp;tv_list);  //  移除需要迁移的链表
 
	/*
	 * We are removing _all_ timers from the list, so we
	 * don&#039;t have to detach them individually.
	 */
	list_for_each_entry_safe(timer, tmp, &amp;tv_list, entry) {
		BUG_ON(tbase_get_base(timer-&gt;base) != base);
                //  重新加入到定时器系统中，实际上将会迁移到下一级的tv数组中
		internal_add_timer(base, timer);  
	}
 
	return index;
}</code></pre></div><p>每个tick事件到来时，内核会在tick定时中断处理期间激活定时器软中断：TIMER_SOFTIRQ，关于软件中断，请参考另一篇博文： Linux中断（interrupt）子系统之五：软件中断（softIRQ。TIMER_SOFTIRQ的执行函数是__run_timers，它实现了本节讨论的逻辑，取出tv1中到期的定时器，执行定时器的回调函数，由此可见， 低分辨率定时器的回调函数是执行在软件中断上下文中的，这点在写定时器的回调函数时需要注意。__run_timers的代码如下： </p><div class="codebox"><pre class="vscroll"><code>static inline void __run_timers(struct tvec_base *base)
{
	struct timer_list *timer;
 
	spin_lock_irq(&amp;base-&gt;lock);
        /* 同步jiffies，在NO_HZ情况下，base-&gt;timer_jiffies可能落后不止一个tick  */
	while (time_after_eq(jiffies, base-&gt;timer_jiffies)) {  
		struct list_head work_list;
		struct list_head *head = &amp;work_list;
                /*  计算到期定时器链表在tv1中的索引  */
		int index = base-&gt;timer_jiffies &amp; TVR_MASK;  
 
		/*
		 * /*  tv2--tv5定时器列表迁移处理  */
		 */
		if (!index &amp;&amp;
			(!cascade(base, &amp;base-&gt;tv2, INDEX(0))) &amp;&amp;              
				(!cascade(base, &amp;base-&gt;tv3, INDEX(1))) &amp;&amp;      
					!cascade(base, &amp;base-&gt;tv4, INDEX(2)))  
			cascade(base, &amp;base-&gt;tv5, INDEX(3));  
                /*  该cpu定时器系统运行时间递增一个tick  */                 
		++base-&gt;timer_jiffies;  
                /*  取出到期的定时器链表  */                                       
		list_replace_init(base-&gt;tv1.vec + index, &amp;work_list);
                /*  遍历所有的到期定时器  */          
		while (!list_empty(head)) {                                    
			void (*fn)(unsigned long);
			unsigned long data;
 
			timer = list_first_entry(head, struct timer_list,entry);
			fn = timer-&gt;function;
			data = timer-&gt;data;
 
			timer_stats_account_timer(timer);
 
			base-&gt;running_timer = timer;    /*  标记正在处理的定时器  */
			detach_timer(timer, 1);
 
			spin_unlock_irq(&amp;base-&gt;lock);
			call_timer_fn(timer, fn, data);  /*  调用定时器的回调函数  */
			spin_lock_irq(&amp;base-&gt;lock);
		}
	}
	base-&gt;running_timer = NULL;
	spin_unlock_irq(&amp;base-&gt;lock);
}</code></pre></div><p>通过上面的讨论，我们可以发现，内核的低分辨率定时器的实现非常精妙，既实现了大量定时器的管理，又实现了快速的O(1)查找到期定时器的能力，利用巧妙的数组结构，使得只需在间隔256个tick时间才处理一次迁移操作，5个数组就好比是5个齿轮，它们随着base-&gt;timer_jifffies的增长而不停地转动，每次只需处理第一个齿轮的某一个齿节，低一级的齿轮转动一圈，高一级的齿轮转动一个齿，同时自动把即将到期的定时器迁移到上一个齿轮中，所以低分辨率定时器通常又被叫做时间轮：time wheel。事实上，它的实现是一个很好的空间换时间软件算法。</p><p>3.&#160; 定时器软件中断</p><p>系统初始化时，start_kernel会调用定时器系统的初始化函数init_timers：</p><div class="codebox"><pre><code>void __init init_timers(void)
{      
	int err = timer_cpu_notify(&amp;timers_nb, (unsigned long)CPU_UP_PREPARE, 
				(void *)(long)smp_processor_id());
 
	init_timer_stats();
 
	BUG_ON(err != NOTIFY_OK);
	register_cpu_notifier(&amp;timers_nb);  /* 注册cpu notify，以便在hotplug时在cpu之间进行定时器的迁移 */
	open_softirq(TIMER_SOFTIRQ, run_timer_softirq);
}</code></pre></div><p>可见，open_softirq把run_timer_softirq注册为TIMER_SOFTIRQ的处理函数，另外，当cpu的每个tick事件到来时，在事件处理中断中，update_process_times会被调用，该函数会进一步调用run_local_timers，run_local_timers会触发TIMER_SOFTIRQ软中断： </p><div class="codebox"><pre><code>void run_local_timers(void)
{
	hrtimer_run_queues();
	raise_softirq(TIMER_SOFTIRQ);
}</code></pre></div><p>TIMER_SOFTIRQ的处理函数是run_timer_softirq： </p><div class="codebox"><pre><code>static void run_timer_softirq(struct softirq_action *h)
{
	struct tvec_base *base = __this_cpu_read(tvec_bases);
 
	hrtimer_run_pending();
 
	if (time_after_eq(jiffies, base-&gt;timer_jiffies))
		__run_timers(base);
}</code></pre></div><p>好啦，终于看到__run_timers函数了，2.2节已经介绍过，正是这个函数完成了对到期定时器的处理工作，也完成了时间轮的不停转动。</p>]]></description>
			<author><![CDATA[dummy@example.com (batsom)]]></author>
			<pubDate>Tue, 23 Apr 2024 12:47:50 +0000</pubDate>
			<guid>http://www.gentoo-zh.org/viewtopic.php?id=863&amp;action=new</guid>
		</item>
		<item>
			<title><![CDATA[Gentoo时间子系统之四：定时器的引擎：clock_event_device]]></title>
			<link>http://www.gentoo-zh.org/viewtopic.php?id=862&amp;action=new</link>
			<description><![CDATA[<p>早期的内核版本中，进程的调度基于一个称之为tick的时钟滴答，通常使用时钟中断来定时地产生tick信号，每次tick定时中断都会进行进程的统计和调度，并对tick进行计数，记录在一个jiffies变量中，定时器的设计也是基于jiffies。这时候的内核代码中，几乎所有关于时钟的操作都是在machine级的代码中实现，很多公共的代码要在每个平台上重复实现。随后，随着通用时钟框架的引入，内核需要支持高精度的定时器，为此，通用时间框架为定时器硬件定义了一个标准的接口：clock_event_device，machine级的代码只要按这个标准接口实现相应的硬件控制功能，剩下的与平台无关的特性则统一由通用时间框架层来实现。</p><p>1.&#160; 时钟事件软件架构<br />本系列文章的第一节中，我们曾经讨论了时钟源设备：clocksource，现在又来一个时钟事件设备：clock_event_device，它们有何区别？看名字，好像都是给系统提供时钟的设备，实际上，clocksource不能被编程，没有产生事件的能力，它主要被用于timekeeper来实现对真实时间进行精确的统计，而clock_event_device则是可编程的，它可以工作在周期触发或单次触发模式，系统可以对它进行编程，以确定下一次事件触发的时间，clock_event_device主要用于实现普通定时器和高精度定时器，同时也用于产生tick事件，供给进程调度子系统使用。时钟事件设备与通用时间框架中的其他模块的关系如下图所示： </p><p><span class="postimg"><img src="https://www.batsom.net/usr/uploads/2024/04/2901284100.png" alt="FluxBB bbcode 测试" /></span></p><p>&gt;与clocksource一样，系统中可以存在多个clock_event_device，系统会根据它们的精度和能力，选择合适的clock_event_device对系统提供时钟事件服务。在smp系统中，为了减少处理器间的通信开销，基本上每个cpu都会具备一个属于自己的本地clock_event_device，独立地为该cpu提供时钟事件服务，smp中的每个cpu基于本地的clock_event_device，建立自己的tick_device，普通定时器和高精度定时器。<br />&gt;在软件架构上看，clock_event_device被分为了两层，与硬件相关的被放在了machine层，而与硬件无关的通用代码则被集中到了通用时间框架层，这符合内核对软件的设计需求，平台的开发者只需实现平台相关的接口即可，无需关注复杂的上层时间框架。<br />&gt;tick_device是基于clock_event_device的进一步封装，用于代替原有的时钟滴答中断，给内核提供tick事件，以完成进程的调度和进程信息统计，负载平衡和时间更新等操作。</p><p>2.&#160; 时钟事件设备相关数据结构<br />2.1&#160; struct clock_event_device</p><p>时钟事件设备的核心数据结构是clock_event_device结构，它代表着一个时钟硬件设备，该设备就好像是一个具有事件触发能力（通常就是指中断）的clocksource，它不停地计数，当计数值达到预先编程设定的数值那一刻，会引发一个时钟事件中断，继而触发该设备的事件处理回调函数，以完成对时钟事件的处理。clock_event_device结构的定义如下：</p><div class="codebox"><pre><code>struct clock_event_device {
	void			(*event_handler)(struct clock_event_device *);
	int			(*set_next_event)(unsigned long evt,
						  struct clock_event_device *);
	int			(*set_next_ktime)(ktime_t expires,
						  struct clock_event_device *);
	ktime_t			next_event;
	u64			max_delta_ns;
	u64			min_delta_ns;
	u32			mult;
	u32			shift;
	enum clock_event_mode	mode;
	unsigned int		features;
	unsigned long		retries;
 
	void			(*broadcast)(const struct cpumask *mask);
	void			(*set_mode)(enum clock_event_mode mode,
					    struct clock_event_device *);
	unsigned long		min_delta_ticks;
	unsigned long		max_delta_ticks;
 
	const char		*name;
	int			rating;
	int			irq;
	const struct cpumask	*cpumask;
	struct list_head	list;
} ____cacheline_aligned;</code></pre></div><p>event_handler&#160; 该字段是一个回调函数指针，通常由通用框架层设置，在时间中断到来时，machine底层的的中断服务程序会调用该回调，框架层利用该回调实现对时钟事件的处理。</p><p>set_next_event&#160; 设置下一次时间触发的时间，使用类似于clocksource的cycle计数值（离现在的cycle差值）作为参数。</p><p>set_next_ktime&#160; 设置下一次时间触发的时间，直接使用ktime时间作为参数。</p><p>max_delta_ns&#160; 可设置的最大时间差，单位是纳秒。</p><p>min_delta_ns&#160; 可设置的最小时间差，单位是纳秒。</p><p>mult shift&#160; 与clocksource中的类似，只不过是用于把纳秒转换为cycle。</p><p>mode&#160; 该时钟事件设备的工作模式，两种主要的工作模式分别是：</p><p>&gt;CLOCK_EVT_MODE_PERIODIC&#160; 周期触发模式，设置后按给定的周期不停地触发事件；<br />&gt;CLOCK_EVT_MODE_ONESHOT&#160; 单次触发模式，只在设置好的触发时刻触发一次；</p><p>set_mode&#160; 函数指针，用于设置时钟事件设备的工作模式。</p><p>rating&#160; 表示该设备的精度等级。</p><p>list&#160; 系统中注册的时钟事件设备用该字段挂在全局链表变量clockevent_devices上。<br />2.2&#160; 全局变量clockevent_devices<br />系统中所有注册的clock_event_device都会挂在该链表下面，它在kernel/time/clockevents.c中定义： </p><p>&gt;static LIST_HEAD(clockevent_devices);</p><p>2.3&#160; 全局变量clockevents_chain<br />通用时间框架初始化时会注册一个通知链（NOTIFIER），当系统中的时钟时间设备的状态发生变化时，利用该通知链通知系统的其它模块。 </p><div class="codebox"><pre><code>/* Notification for clock events */
static RAW_NOTIFIER_HEAD(clockevents_chain);</code></pre></div><p>3.&#160; clock_event_device的初始化和注册<br />每一个machine，都要定义一个自己的machine_desc结构，该结构定义了该machine的一些最基本的特性，其中需要设定一个sys_timer结构指针，machine级的代码负责定义sys_timer结构，sys_timer的声明很简单：</p><div class="codebox"><pre><code>struct sys_timer {
	void			(*init)(void);
	void			(*suspend)(void);
	void			(*resume)(void);
#ifdef CONFIG_ARCH_USES_GETTIMEOFFSET
	unsigned long		(*offset)(void);
#endif
};</code></pre></div><p>通常，我们至少要定义它的init字段，系统初始化阶段，该init回调会被调用，该init回调函数的主要作用就是完成系统中的clocksource和clock_event_device的硬件初始化工作，以samsung的exynos4为例，在V3.4内核的代码树中，machine_desc的定义如下： </p><div class="codebox"><pre><code>MACHINE_START(SMDK4412, &quot;SMDK4412&quot;)
	/* Maintainer: Kukjin Kim &lt;kgene.kim@samsung.com&gt; */
	/* Maintainer: Changhwan Youn &lt;chaos.youn@samsung.com&gt; */
	.atag_offset	= 0x100,
	.init_irq	= exynos4_init_irq,
	.map_io		= smdk4x12_map_io,
	.handle_irq	= gic_handle_irq,
	.init_machine	= smdk4x12_machine_init,
	.timer		= &amp;exynos4_timer,
	.restart	= exynos4_restart,
MACHINE_END</code></pre></div><p>定义的sys_timer是exynos4_timer，它的定义和init回调定义如下： </p><div class="codebox"><pre><code>static void __init exynos4_timer_init(void)
{
	if (soc_is_exynos4210())
		mct_int_type = MCT_INT_SPI;
	else
		mct_int_type = MCT_INT_PPI;
 
	exynos4_timer_resources();
	exynos4_clocksource_init();
	exynos4_clockevent_init();
}
 
struct sys_timer exynos4_timer = {
	.init		= exynos4_timer_init,
};</code></pre></div><p>exynos4_clockevent_init函数显然是初始化和注册clock_event_device的合适时机，在这里，它注册了一个rating为250的clock_event_device，并把它指定给cpu0： </p><div class="codebox"><pre><code>static struct clock_event_device mct_comp_device = {
	.name		= &quot;mct-comp&quot;,
	.features       = CLOCK_EVT_FEAT_PERIODIC | CLOCK_EVT_FEAT_ONESHOT,
	.rating		= 250,
	.set_next_event	= exynos4_comp_set_next_event,
	.set_mode	= exynos4_comp_set_mode,
};
......
static void exynos4_clockevent_init(void)
{
	clockevents_calc_mult_shift(&amp;mct_comp_device, clk_rate, 5);
        ......
	mct_comp_device.cpumask = cpumask_of(0);
	clockevents_register_device(&amp;mct_comp_device);
 
	setup_irq(EXYNOS4_IRQ_MCT_G0, &amp;mct_comp_event_irq);
}</code></pre></div><p>因为这个阶段其它cpu核尚未开始工作，所以该clock_event_device也只是在启动阶段给系统提供服务，实际上，因为exynos4是一个smp系统，具备2-4个cpu核心，前面说过，smp系统中，通常会使用各个cpu的本地定时器来为每个cpu单独提供时钟事件服务，继续翻阅代码，在系统初始化的后段，kernel_init会被调用，它会调用smp_prepare_cpus，其中会调用percpu_timer_setup函数，在arch/arm/kernel/smp.c中，为每个cpu定义了一个clock_event_device： </p><div class="codebox"><pre><code>/*
 * Timer (local or broadcast) support
 */
static DEFINE_PER_CPU(struct clock_event_device, percpu_clockevent);</code></pre></div><p>percpu_timer_setup最终会调用exynos4_local_timer_setup函数完成对本地clock_event_device的初始化工作： </p><div class="codebox"><pre><code>static int __cpuinit exynos4_local_timer_setup(struct clock_event_device *evt)
{
    ......
	evt-&gt;name = mevt-&gt;name;
	evt-&gt;cpumask = cpumask_of(cpu);
	evt-&gt;set_next_event = exynos4_tick_set_next_event;
	evt-&gt;set_mode = exynos4_tick_set_mode;
	evt-&gt;features = CLOCK_EVT_FEAT_PERIODIC | CLOCK_EVT_FEAT_ONESHOT;
	evt-&gt;rating = 450;
 
	clockevents_calc_mult_shift(evt, clk_rate / (TICK_BASE_CNT + 1), 5);
    ......
	clockevents_register_device(evt);
    ......
	enable_percpu_irq(EXYNOS_IRQ_MCT_LOCALTIMER, 0);
    ......
	return 0;
}</code></pre></div><p>由此可见，每个cpu的本地clock_event_device的rating是450，比启动阶段的250要高，显然，之前注册给cpu0的精度要高，系统会用本地clock_event_device替换掉原来分配给cpu0的clock_event_device，至于怎么替换？我们先停一停，到这里我们一直在讨论machine级别的初始化和注册，让我们回过头来，看看框架层的初始化。在继续之前，让我们看看整个clock_event_device的初始化的调用序列图： </p><p><span class="postimg"><img src="https://www.batsom.net/usr/uploads/2024/04/2467323694.png" alt="FluxBB bbcode 测试" /></span></p><p>由上面的图示可以看出，框架层的初始化步骤很简单，又start_kernel开始，调用tick_init，它位于kernel/time/tick-common.c中，也只是简单地调用clockevents_register_notifier，同时把类型为notifier_block的tick_notifier作为参数传入，回看2.3节，clockevents_register_notifier注册了一个通知链，这样，当系统中的clock_event_device状态发生变化时（新增，删除，挂起，唤醒等等），tick_notifier中的notifier_call字段中设定的回调函数tick_notify就会被调用。接下来start_kernel调用了time_init函数，该函数通常定义在体系相关的代码中，正如前面所讨论的一样，它主要完成machine级别对时钟系统的初始化工作，最终通过clockevents_register_device注册系统中的时钟事件设备，把每个时钟时间设备挂在clockevent_device全局链表上，最后通过clockevent_do_notify触发框架层事先注册好的通知链，其实就是调用了tick_notify函数，我们主要关注CLOCK_EVT_NOTIFY_ADD通知，其它通知请自行参考代码，下面是tick_notify的简化版本：</p><div class="codebox"><pre><code>static int tick_notify(struct notifier_block *nb, unsigned long reason,
			       void *dev)
{
	switch (reason) {
 
	case CLOCK_EVT_NOTIFY_ADD:
		return tick_check_new_device(dev);
 
	case CLOCK_EVT_NOTIFY_BROADCAST_ON:
	case CLOCK_EVT_NOTIFY_BROADCAST_OFF:
	case CLOCK_EVT_NOTIFY_BROADCAST_FORCE:
            ......
	case CLOCK_EVT_NOTIFY_BROADCAST_ENTER:
	case CLOCK_EVT_NOTIFY_BROADCAST_EXIT:
            ......
	case CLOCK_EVT_NOTIFY_CPU_DYING:
            ......
	case CLOCK_EVT_NOTIFY_CPU_DEAD:
            ......
	case CLOCK_EVT_NOTIFY_SUSPEND:
            ......
	case CLOCK_EVT_NOTIFY_RESUME:
            ......
	}
 
	return NOTIFY_OK;
}</code></pre></div><p>可见，对于新注册的clock_event_device，会发出CLOCK_EVT_NOTIFY_ADD通知，最终会进入函数：tick_check_new_device，这个函数比对当前cpu所使用的与新注册的clock_event_device之间的特性，如果认为新的clock_event_device更好，则会进行切换工作。下一节将会详细的讨论该函数。到这里，每个cpu已经有了自己的clock_event_device，在这以后，框架层的代码会根据内核的配置项（CONFIG_NO_HZ、CONFIG_HIGH_RES_TIMERS），对注册的clock_event_device进行不同的设置，从而为系统的tick和高精度定时器提供服务，这些内容我们留在本系列的后续文章进行讨论。</p><p>4.&#160; tick_device<br />当内核没有配置成支持高精度定时器时，系统的tick由tick_device产生，tick_device其实是clock_event_device的简单封装，它内嵌了一个clock_event_device指针和它的工作模式： </p><div class="codebox"><pre><code>struct tick_device {
	struct clock_event_device *evtdev;
	enum tick_device_mode mode;
};</code></pre></div><p>在kernel/time/tick-common.c中，定义了一个per-cpu的tick_device全局变量，tick_cpu_device： </p><div class="codebox"><pre><code>/*
 * Tick devices
 */
DEFINE_PER_CPU(struct tick_device, tick_cpu_device);</code></pre></div><p>前面曾经说过，当machine的代码为每个cpu注册clock_event_device时，通知回调函数tick_notify会被调用，进而进入tick_check_new_device函数，下面让我们看看该函数如何工作，首先，该函数先判断注册的clock_event_device是否可用于本cpu，然后从per-cpu变量中取出本cpu的tick_device： </p><div class="codebox"><pre><code>static int tick_check_new_device(struct clock_event_device *newdev)
{
        ......
	cpu = smp_processor_id();
	if (!cpumask_test_cpu(cpu, newdev-&gt;cpumask))
		goto out_bc;
 
	td = &amp;per_cpu(tick_cpu_device, cpu);
	curdev = td-&gt;evtdev;</code></pre></div><p>如果不是本地clock_event_device，会做进一步的判断：如果不能把irq绑定到本cpu，则放弃处理，如果本cpu已经有了一个本地clock_event_device，也放弃处理：</p><div class="codebox"><pre><code>	if (!cpumask_equal(newdev-&gt;cpumask, cpumask_of(cpu))) {
                ......
		if (!irq_can_set_affinity(newdev-&gt;irq))
			goto out_bc;
                ......
		if (curdev &amp;&amp; cpumask_equal(curdev-&gt;cpumask, cpumask_of(cpu)))
			goto out_bc;
	}</code></pre></div><p>反之，如果本cpu已经有了一个clock_event_device，则根据是否支持单触发模式和它的rating值，决定是否替换原来旧的clock_event_device： </p><div class="codebox"><pre><code>	if (curdev) {
		if ((curdev-&gt;features &amp; CLOCK_EVT_FEAT_ONESHOT) &amp;&amp;
		    !(newdev-&gt;features &amp; CLOCK_EVT_FEAT_ONESHOT))
			goto out_bc;  // 新的不支持单触发，但旧的支持，所以不能替换
		if (curdev-&gt;rating &gt;= newdev-&gt;rating)
			goto out_bc;  // 旧的比新的精度高，不能替换
	}</code></pre></div><p>在这些判断都通过之后，说明或者来cpu还没有绑定tick_device，或者是新的更好，需要替换： </p><div class="codebox"><pre><code>	if (tick_is_broadcast_device(curdev)) {
		clockevents_shutdown(curdev);
		curdev = NULL;
	}
	clockevents_exchange_device(curdev, newdev);
	tick_setup_device(td, newdev, cpu, cpumask_of(cpu));</code></pre></div><p>上面的tick_setup_device函数负责重新绑定当前cpu的tick_device和新注册的clock_event_device，如果发现是当前cpu第一次注册tick_device，就把它设置为TICKDEV_MODE_PERIODIC模式，如果是替换旧的tick_device，则根据新的tick_device的特性，设置为TICKDEV_MODE_PERIODIC或TICKDEV_MODE_ONESHOT模式。可见，在系统的启动阶段，tick_device是工作在周期触发模式的，直到框架层在合适的时机，才会开启单触发模式，以便支持NO_HZ和HRTIMER。<br />5.&#160; tick事件的处理--最简单的情况<br />clock_event_device最基本的应用就是实现tick_device，然后给系统定期地产生tick事件，通用时间框架对clock_event_device和tick_device的处理相当复杂，因为涉及配置项：CONFIG_NO_HZ和CONFIG_HIGH_RES_TIMERS的组合，两个配置项就有4种组合，这四种组合的处理都有所不同，所以这里我先只讨论最简单的情况：</p><p>&gt;CONFIG_NO_HZ == 0;<br />&gt;CONFIG_HIGH_RES_TIMERS == 0;</p><p>在这种配置模式下，我们回到上一节的tick_setup_device函数的最后： </p><div class="codebox"><pre><code>	if (td-&gt;mode == TICKDEV_MODE_PERIODIC)
		tick_setup_periodic(newdev, 0);
	else
		tick_setup_oneshot(newdev, handler, next_event);</code></pre></div><p>因为启动期间，第一个注册的tick_device必然工作在TICKDEV_MODE_PERIODIC模式，所以tick_setup_periodic会设置clock_event_device的事件回调字段event_handler为tick_handle_periodic，工作一段时间后，就算有新的支持TICKDEV_MODE_ONESHOT模式的clock_event_device需要替换，再次进入tick_setup_device函数，tick_setup_oneshot的handler参数也是之前设置的tick_handle_periodic函数，所以我们考察tick_handle_periodic即可： </p><div class="codebox"><pre><code>void tick_handle_periodic(struct clock_event_device *dev)
{
	int cpu = smp_processor_id();
	ktime_t next;
 
	tick_periodic(cpu);
 
	if (dev-&gt;mode != CLOCK_EVT_MODE_ONESHOT)
		return;
 
	next = ktime_add(dev-&gt;next_event, tick_period);
	for (;;) {
		if (!clockevents_program_event(dev, next, false))
			return;
		if (timekeeping_valid_for_hres())
			tick_periodic(cpu);
		next = ktime_add(next, tick_period);
	}
}</code></pre></div><p>该函数首先调用tick_periodic函数，完成tick事件的所有处理，如果是周期触发模式，处理结束，如果工作在单触发模式，则计算并设置下一次的触发时刻，这里用了一个循环，是为了防止当该函数被调用时，clock_event_device中的计时实际上已经经过了不止一个tick周期，这时候，tick_periodic可能被多次调用，使得jiffies和时间可以被正确地更新。tick_periodic的代码如下： </p><div class="codebox"><pre><code>static void tick_periodic(int cpu)
{
	if (tick_do_timer_cpu == cpu) {
		write_seqlock(&amp;xtime_lock);
 
		/* Keep track of the next tick event */
		tick_next_period = ktime_add(tick_next_period, tick_period);
 
		do_timer(1);
		write_sequnlock(&amp;xtime_lock);
	}
 
	update_process_times(user_mode(get_irq_regs()));
	profile_tick(CPU_PROFILING);
}</code></pre></div><p>如果当前cpu负责更新时间，则通过do_timer进行以下操作：<br />&gt;更新jiffies_64变量；<br />&gt;更新墙上时钟；<br />&gt;每10个tick，更新一次cpu的负载信息；</p><p>调用update_peocess_times，完成以下事情：<br />&gt;更新进程的时间统计信息；<br />&gt;触发TIMER_SOFTIRQ软件中断，以便系统处理传统的低分辨率定时器；<br />&gt;检查rcu的callback；<br />&gt;通过scheduler_tick触发调度系统进行进程统计和调度工作；</p>]]></description>
			<author><![CDATA[dummy@example.com (batsom)]]></author>
			<pubDate>Tue, 23 Apr 2024 12:43:52 +0000</pubDate>
			<guid>http://www.gentoo-zh.org/viewtopic.php?id=862&amp;action=new</guid>
		</item>
		<item>
			<title><![CDATA[Gentoo时间子系统之三：时间的维护者：timekeeper]]></title>
			<link>http://www.gentoo-zh.org/viewtopic.php?id=861&amp;action=new</link>
			<description><![CDATA[<p>本系列文章的前两节讨论了用于计时的时钟源：clocksource，以及内核内部时间的一些表示方法，但是对于真实的用户来说，我们感知的是真实世界的真实时间，也就是所谓的墙上时间，clocksource只能提供一个按给定频率不停递增的周期计数，如何把它和真实的墙上时间相关联？本节的内容正是要讨论这一点。<br />1.&#160; 时间的种类</p><p>内核管理着多种时间，它们分别是：<br />&gt;RTC时间<br />&gt;wall time：墙上时间<br />&gt;monotonic time<br />&gt;raw monotonic time<br />&gt;boot time：总启动时间</p><p>RTC时间&#160; 在PC中，RTC时间又叫CMOS时间，它通常由一个专门的计时硬件来实现，软件可以读取该硬件来获得年月日、时分秒等时间信息，而在嵌入式系统中，有使用专门的RTC芯片，也有直接把RTC集成到Soc芯片中，读取Soc中的某个寄存器即可获取当前时间信息。一般来说，RTC是一种可持续计时的，也就是说，不管系统是否上电，RTC中的时间信息都不会丢失，计时会一直持续进行，硬件上通常使用一个后备电池对RTC硬件进行单独的供电。因为RTC硬件的多样性，开发者需要为每种RTC时钟硬件提供相应的驱动程序，内核和用户空间通过驱动程序访问RTC硬件来获取或设置时间信息。</p><p>xtime&#160; xtime和RTC时间一样，都是人们日常所使用的墙上时间，只是RTC时间的精度通常比较低，大多数情况下只能达到毫秒级别的精度，如果是使用外部的RTC芯片，访问速度也比较慢，为此，内核维护了另外一个wall time时间：xtime，取决于用于对xtime计时的clocksource，它的精度甚至可以达到纳秒级别，因为xtime实际上是一个内存中的变量，它的访问速度非常快，内核大部分时间都是使用xtime来获得当前时间信息。xtime记录的是自1970年1月1日24时到当前时刻所经历的纳秒数。</p><p>monotonic time&#160; 该时间自系统开机后就一直单调地增加，它不像xtime可以因用户的调整时间而产生跳变，不过该时间不计算系统休眠的时间，也就是说，系统休眠时，monotoic时间不会递增。</p><p>raw monotonic time&#160; 该时间与monotonic时间类似，也是单调递增的时间，唯一的不同是：raw monotonic time“更纯净”，他不会受到NTP时间调整的影响，它代表着系统独立时钟硬件对时间的统计。</p><p>boot time&#160; 与monotonic时间相同，不过会累加上系统休眠的时间，它代表着系统上电后的总时间。</p><div class="codebox"><pre class="vscroll"><code>struct timekeeper {
	struct clocksource *clock;    /* Current clocksource used for timekeeping. */
	u32	mult;    /* NTP adjusted clock multiplier */
	int	shift;	/* The shift value of the current clocksource. */
	cycle_t cycle_interval;	/* Number of clock cycles in one NTP interval. */
	u64	xtime_interval;	/* Number of clock shifted nano seconds in one NTP interval. */
	s64	xtime_remainder;	/* shifted nano seconds left over when rounding cycle_interval */
	u32	raw_interval;	/* Raw nano seconds accumulated per NTP interval. */
 
	u64	xtime_nsec;	/* Clock shifted nano seconds remainder not stored in xtime.tv_nsec. */
	/* Difference between accumulated time and NTP time in ntp
	 * shifted nano seconds. */
	s64	ntp_error;
	/* Shift conversion between clock shifted nano seconds and
	 * ntp shifted nano seconds. */
	int	ntp_error_shift;
 
	struct timespec xtime;	/* The current time */
 
	struct timespec wall_to_monotonic;
	struct timespec total_sleep_time;	/* time spent in suspend */
	struct timespec raw_time;	/* The raw monotonic time for the CLOCK_MONOTONIC_RAW posix clock. */
 
	ktime_t offs_real;	/* Offset clock monotonic -&gt; clock realtime */
 
	ktime_t offs_boot;	/* Offset clock monotonic -&gt; clock boottime */
 
	seqlock_t lock;	/* Seqlock for all timekeeper values */
};</code></pre></div><p>其中的xtime字段就是上面所说的墙上时间，它是一个timespec结构的变量，它记录了自1970年1月1日以来所经过的时间，因为是timespec结构，所以它的精度可以达到纳秒级，当然那要取决于系统的硬件是否支持这一精度。</p><p>内核除了用xtime表示墙上的真实时间外，还维护了另外一个时间：monotonic time，可以把它理解为自系统启动以来所经过的时间，该时间只能单调递增，可以理解为xtime虽然正常情况下也是递增的，但是毕竟用户可以主动向前或向后调整墙上时间，从而修改xtime值。但是monotonic时间不可以往后退，系统启动后只能不断递增。奇怪的是，内核并没有直接定义一个这样的变量来记录monotonic时间，而是定义了一个变量wall_to_monotonic，记录了墙上时间和monotonic时间之间的偏移量，当需要获得monotonic时间时，把xtime和wall_to_monotonic相加即可，因为默认启动时monotonic时间为0，所以实际上wall_to_monotonic的值是一个负数，它和xtime同一时间被初始化，请参考timekeeping_init函数。</p><p>计算monotonic时间要去除系统休眠期间花费的时间，内核用total_sleep_time记录休眠的时间，每次休眠醒来后重新累加该时间，并调整wall_to_monotonic的值，使其在系统休眠醒来后，monotonic时间不会发生跳变。因为wall_to_monotonic值被调整。所以如果想获取boot time，需要加入该变量的值：</p><div class="codebox"><pre><code>void get_monotonic_boottime(struct timespec *ts)
{
        ......
	do {
		seq = read_seqbegin(&amp;timekeeper.lock);
		*ts = timekeeper.xtime;
		tomono = timekeeper.wall_to_monotonic;
		sleep = timekeeper.total_sleep_time;
		nsecs = timekeeping_get_ns();
 
	} while (read_seqretry(&amp;timekeeper.lock, seq));
 
	set_normalized_timespec(ts, ts-&gt;tv_sec + tomono.tv_sec + sleep.tv_sec,
			ts-&gt;tv_nsec + tomono.tv_nsec + sleep.tv_nsec + nsecs);
}</code></pre></div><p>raw_time字段用来表示真正的硬件时间，也就是上面所说的raw monotonic time，它不受时间调整的影响，monotonic时间虽然也不受settimeofday的影响，但会受到ntp调整的影响，但是raw_time不受ntp的影响，他真的就是开完机后就单调地递增。xtime、monotonic-time和raw_time可以通过用户空间的clock_gettime函数获得，对应的ID参数分别是 CLOCK_REALTIME、CLOCK_MONOTONIC、CLOCK_MONOTONIC_RAW。</p><p>clock字段则指向了目前timekeeper所使用的时钟源，xtime，monotonic time和raw time都是基于该时钟源进行计时操作，当有新的精度更高的时钟源被注册时，通过timekeeping_notify函数，change_clocksource函数将会被调用，timekeeper.clock字段将会被更新，指向新的clocksource。</p><p>早期的内核版本中，xtime、wall_to_monotonic、raw_time其实是定义为全局静态变量，到我目前的版本（V3.4.10），这几个变量被移入到了timekeeper结构中，现在只需维护一个timekeeper全局静态变量即可：</p><p>&gt;static struct timekeeper timekeeper;</p><p>3.&#160; timekeeper的初始化</p><p>timekeeper的初始化由timekeeping_init完成，该函数在start_kernel的初始化序列中被调用，timekeeping_init首先从RTC中获取当前时间：</p><div class="codebox"><pre><code>void __init timekeeping_init(void)
{
	struct clocksource *clock;
	unsigned long flags;
	struct timespec now, boot;
 
	read_persistent_clock(&amp;now);
	read_boot_clock(&amp;boot);</code></pre></div><p>然后对锁和ntp进行必要的初始化： </p><div class="codebox"><pre><code>	seqlock_init(&amp;timekeeper.lock);
 
	ntp_init();</code></pre></div><p>利用RTC的当前时间，初始化xtime，raw_time，wall_to_monotonic等字段： </p><div class="codebox"><pre><code>	timekeeper.xtime.tv_sec = now.tv_sec;
	timekeeper.xtime.tv_nsec = now.tv_nsec;
	timekeeper.raw_time.tv_sec = 0;
	timekeeper.raw_time.tv_nsec = 0;
	if (boot.tv_sec == 0 &amp;&amp; boot.tv_nsec == 0) {
		boot.tv_sec = timekeeper.xtime.tv_sec;
		boot.tv_nsec = timekeeper.xtime.tv_nsec;
	}
	set_normalized_timespec(&amp;timekeeper.wall_to_monotonic,
				-boot.tv_sec, -boot.tv_nsec);</code></pre></div><p>最后，初始化代表实时时间和monotonic时间之间偏移量的offs_real字段，total_sleep_time字段初始化为0： </p><div class="codebox"><pre><code>	update_rt_offset();
	timekeeper.total_sleep_time.tv_sec = 0;
	timekeeper.total_sleep_time.tv_nsec = 0;
	write_sequnlock_irqrestore(&amp;timekeeper.lock, flags);</code></pre></div><p>xtime字段因为是保存在内存中，系统掉电后无法保存时间信息，所以每次启动时都要通过timekeeping_init从RTC中同步正确的时间信息。其中，read_persistent_clock和read_boot_clock是平台级的函数，分别用于获取RTC硬件时间和启动时的时间，不过值得注意到是，到目前为止（我的代码树基于3.4版本），ARM体系中，只有tegra和omap平台实现了read_persistent_clock函数。如果平台没有实现该函数，内核提供了一个默认的实现： </p><div class="codebox"><pre><code>void __attribute__((weak)) read_persistent_clock(struct timespec *ts)
{
	ts-&gt;tv_sec = 0;
	ts-&gt;tv_nsec = 0;
}</code></pre></div><div class="codebox"><pre><code>void __attribute__((weak)) read_boot_clock(struct timespec *ts)
{
	ts-&gt;tv_sec = 0;
	ts-&gt;tv_nsec = 0;
}</code></pre></div><p>那么，其他ARM平台是如何初始化xtime的？答案就是CONFIG_RTC_HCTOSYS这个内核配置项，打开该配置后，driver/rtc/hctosys.c将会编译到系统中，由rtc_hctosys函数通过do_settimeofday在系统初始化时完成xtime变量的初始化： </p><div class="codebox"><pre><code>static int __init rtc_hctosys(void) 
{ 
        ...... 
        err = rtc_read_time(rtc, &amp;tm); 
        ......
        rtc_tm_to_time(&amp;tm, &amp;tv.tv_sec); 
        do_settimeofday(&amp;tv); 
        ...... 
        return err; 
} 
late_initcall(rtc_hctosys);</code></pre></div><p>4.&#160; 时间的更新</p><p>xtime一旦初始化完成后，timekeeper就开始独立于RTC，利用自身关联的clocksource进行时间的更新操作，根据内核的配置项的不同，更新时间的操作发生的频度也不尽相同，如果没有配置NO_HZ选项，通常每个tick的定时中断周期，do_timer会被调用一次，相反，如果配置了NO_HZ选项，可能会在好几个tick后，do_timer才会被调用一次，当然传入的参数是本次更新离上一次更新时相隔了多少个tick周期，系统会保证在clocksource的max_idle_ns时间内调用do_timer，以防止clocksource的溢出：</p><div class="codebox"><pre><code>void do_timer(unsigned long ticks)
{
	jiffies_64 += ticks;
	update_wall_time();
	calc_global_load(ticks);
}</code></pre></div><p>在do_timer中，jiffies_64变量被相应地累加，然后在update_wall_time中完成xtime等时间的更新操作，更新时间的核心操作就是读取关联clocksource的计数值，累加到xtime等字段中，其中还设计ntp时间的调整等代码，详细的代码就不贴了。</p><p>5.&#160; 获取时间</p><p>timekeeper提供了一系列的接口用于获取各种时间信息。<br />&gt;void getboottime(struct timespec *ts);&#160; &#160; 获取系统启动时刻的实时时间<br />&gt;void get_monotonic_boottime(struct timespec *ts);&#160; &#160; &#160;获取系统启动以来所经过的时间，包含休眠时间<br />&gt;ktime_t ktime_get_boottime(void);&#160; &#160;获取系统启动以来所经过的c时间，包含休眠时间，返回ktime类型<br />&gt;ktime_t ktime_get(void);&#160; &#160; 获取系统启动以来所经过的c时间，不包含休眠时间，返回ktime类型<br />&gt;void ktime_get_ts(struct timespec *ts) ;&#160; &#160;获取系统启动以来所经过的c时间，不包含休眠时间，返回timespec结构<br />&gt;unsigned long get_seconds(void);&#160; &#160; 返回xtime中的秒计数值<br />&gt;struct timespec current_kernel_time(void);&#160; &#160; 返回内核最后一次更新的xtime时间，不累计最后一次更新至今clocksource的计数值<br />&gt;void getnstimeofday(struct timespec *ts);&#160; &#160; 获取当前时间，返回timespec结构<br />&gt;void do_gettimeofday(struct timeval *tv);&#160; &#160; 获取当前时间，返回timeval结构</p><br /><p><span class="postimg"><img src="https://www.batsom.net/usr/uploads/2024/04/3713159613.png" alt="FluxBB bbcode 测试" /></span></p>]]></description>
			<author><![CDATA[dummy@example.com (batsom)]]></author>
			<pubDate>Tue, 23 Apr 2024 12:41:20 +0000</pubDate>
			<guid>http://www.gentoo-zh.org/viewtopic.php?id=861&amp;action=new</guid>
		</item>
		<item>
			<title><![CDATA[Gentoo时间子系统之二：表示时间的单位和结构]]></title>
			<link>http://www.gentoo-zh.org/viewtopic.php?id=860&amp;action=new</link>
			<description><![CDATA[<p>人们习惯用于表示时间的方法是：年、月、日、时、分、秒、毫秒、星期等等，但是在内核中，为了软件逻辑和代码的方便性，它使用了一些不同的时间表示方法，并为这些表示方法定义了相应的变量和数据结构，本节的内容就是阐述这些表示方法的意义和区别。</p><br /><p>/*****************************************************************************************************/<br />声明：本博内容均由http://blog.csdn.net/droidphone原创，转载请注明出处，谢谢！</p><p>/*****************************************************************************************************/<br />1.&#160; jiffies</p><p>内核用jiffies变量记录系统启动以来经过的时钟滴答数，它的声明如下：</p><div class="codebox"><pre><code>extern u64 __jiffy_data jiffies_64;
extern unsigned long volatile __jiffy_data jiffies;</code></pre></div><p>可见，在32位的系统上，jiffies是一个32位的无符号数，系统每过1/HZ秒，jiffies的值就会加1，最终该变量可能会溢出，所以内核同时又定义了一个64位的变量jiffies_64，链接的脚本保证jiffies变量和jiffies_64变量的内存地址是相同的，通常，我们可以直接访问jiffies变量，但是要获得jiffies_64变量，必须通过辅助函数get_jiffies_64来实现。jiffies是内核的低精度定时器的计时单位，所以内核配置的HZ数决定了低精度定时器的精度，如果HZ数被设定为1000，那么，低精度定时器（timer_list）的精度就是1ms=1/1000秒。因为jiffies变量可能存在溢出的问题，所以在用基于jiffies进行比较时，应该使用以下辅助宏来实现：</p><div class="codebox"><pre><code>time_after(a,b)
time_before(a,b)
time_after_eq(a,b)
time_before_eq(a,b)
time_in_range(a,b,c)</code></pre></div><p>同时，内核还提供了一些辅助函数用于jiffies和毫秒以及纳秒之间的转换：</p><div class="codebox"><pre><code>unsigned int jiffies_to_msecs(const unsigned long j);
unsigned int jiffies_to_usecs(const unsigned long j);
unsigned long msecs_to_jiffies(const unsigned int m);
unsigned long usecs_to_jiffies(const unsigned int u);</code></pre></div><p>2.&#160; struct timeval<br />timeval由秒和微秒组成，它的定义如下： </p><div class="codebox"><pre><code>struct timeval {
	__kernel_time_t		tv_sec;		/* seconds */
	__kernel_suseconds_t	tv_usec;	/* microseconds */
};</code></pre></div><p>__kernel_time_t&#160; 和 __kernel_suseconds_t 实际上都是long型的整数。gettimeofday和settimeofday使用timeval作为时间单位。<br />3.&#160; struct timespec<br />timespec由秒和纳秒组成，它的定义如下： </p><div class="codebox"><pre><code>struct timespec {
	__kernel_time_t	tv_sec;			/* seconds */
	long		tv_nsec;		/* nanoseconds */
};</code></pre></div><p>同样地，内核也提供了一些辅助函数用于jiffies、timeval、timespec之间的转换： </p><div class="codebox"><pre><code>static inline int timespec_equal(const struct timespec *a, const struct timespec *b);
static inline int timespec_compare(const struct timespec *lhs, const struct timespec *rhs);
static inline int timeval_compare(const struct timeval *lhs, const struct timeval *rhs);
extern unsigned long mktime(const unsigned int year, const unsigned int mon,
			    const unsigned int day, const unsigned int hour,
			    const unsigned int min, const unsigned int sec);
extern void set_normalized_timespec(struct timespec *ts, time_t sec, s64 nsec);
static inline struct timespec timespec_add(struct timespec lhs,	struct timespec rhs);
static inline struct timespec timespec_sub(struct timespec lhs,	struct timespec rhs);
 
static inline s64 timespec_to_ns(const struct timespec *ts);
static inline s64 timeval_to_ns(const struct timeval *tv);
extern struct timespec ns_to_timespec(const s64 nsec);
extern struct timeval ns_to_timeval(const s64 nsec);
static __always_inline void timespec_add_ns(struct timespec *a, u64 ns);</code></pre></div><div class="codebox"><pre><code>unsigned long timespec_to_jiffies(const struct timespec *value);
void jiffies_to_timespec(const unsigned long jiffies, struct timespec *value);
unsigned long timeval_to_jiffies(const struct timeval *value);
void jiffies_to_timeval(const unsigned long jiffies, struct timeval *value);</code></pre></div><p>timekeeper中的xtime字段用timespec作为时间单位。<br />4.&#160; struct ktime<br />linux的通用时间架构用ktime来表示时间，为了兼容32位和64位以及big-little endian系统，ktime结构被定义如下：</p><div class="codebox"><pre><code>union ktime {
	s64	tv64;
#if BITS_PER_LONG != 64 &amp;&amp; !defined(CONFIG_KTIME_SCALAR)
	struct {
# ifdef __BIG_ENDIAN
	s32	sec, nsec;
# else
	s32	nsec, sec;
# endif
	} tv;
#endif
};</code></pre></div><p>64位的系统可以直接访问tv64字段，单位是纳秒，32位的系统则被拆分为两个字段：sec和nsec，并且照顾了大小端的不同。高精度定时器通常用ktime作为计时单位。下面是一些辅助函数用于计算和转换： </p><div class="codebox"><pre><code>ktime_t ktime_set(const long secs, const unsigned long nsecs); 
ktime_t ktime_sub(const ktime_t lhs, const ktime_t rhs); 
ktime_t ktime_add(const ktime_t add1, const ktime_t add2); 
ktime_t ktime_add_ns(const ktime_t kt, u64 nsec); 
ktime_t ktime_sub_ns(const ktime_t kt, u64 nsec); 
ktime_t timespec_to_ktime(const struct timespec ts); 
ktime_t timeval_to_ktime(const struct timeval tv); 
struct timespec ktime_to_timespec(const ktime_t kt); 
struct timeval ktime_to_timeval(const ktime_t kt); 
s64 ktime_to_ns(const ktime_t kt); 
int ktime_equal(const ktime_t cmp1, const ktime_t cmp2); 
s64 ktime_to_us(const ktime_t kt); 
s64 ktime_to_ms(const ktime_t kt); 
ktime_t ns_to_ktime(u64 ns);</code></pre></div>]]></description>
			<author><![CDATA[dummy@example.com (batsom)]]></author>
			<pubDate>Tue, 23 Apr 2024 12:38:34 +0000</pubDate>
			<guid>http://www.gentoo-zh.org/viewtopic.php?id=860&amp;action=new</guid>
		</item>
	</channel>
</rss>
