-
Notifications
You must be signed in to change notification settings - Fork 0
/
en.search-data.min.f6c582906819af733b874a5ad67d92d67f44a163d4515152ded9b24295aa760d.json
1 lines (1 loc) · 80.9 KB
/
en.search-data.min.f6c582906819af733b874a5ad67d92d67f44a163d4515152ded9b24295aa760d.json
1
[{"id":0,"href":"/docs/notes/log_on_linux/","title":"Logging On Linux","section":"Docs","content":" 简单回顾一些Linux上日志系统的一些背景,并配置一个rsyslog的日志结构化落db的系统。\n 背景 push模型:应用程序自己主动推日志到消息系统\npull模型:消息系统监控应用程序的日志,自己去query\nsyslog 系:syslog-\u0026gt;rsyslog or syslog-ng push 模型,监听/dev/log,收到的日志一般存放在/var/log 下面。配置在/etc/rsyslog.conf\n日志格式 有 2 个 rfc\nrfc 3164 BSD style 这个更简单一点,最大长度 1024\n\u0026lt;35\u0026gt;Oct 12 22:14:15 client_machine su: \u0026#39;su root\u0026#39; failed for joe on /dev/pts/2 field explain \u0026lt;35\u0026gt; priority = facility + severity Oct 12 22:14:15 日期 格式只能是这个 client_machine 主机名称、ip 地址 su: tag,最多 32B,以[ : space 结束 \u0026lsquo;su root\u0026rsquo; fail。。。 消息正文 rfc 5424 2009 年出的,最大长度 2048\n\u0026lt;34\u0026gt;1 2003-10-11T22:14:15.003Z mymachine.example.com su - ID47 - BOM\u0026#39;su root\u0026#39; failed for lonvick on /dev/pts/8 field explain \u0026lt;34\u0026gt; pri 1 version 2003-10-11T22:14:15.003Z 时间 mymachine.example.com 机器 su app name - 应该是进程 pid ID47 消息 ID - 结构化数据 BOM\u0026rsquo;su root\u0026rsquo; failed for\u0026hellip; 消息正文 再来一个有结构化数据的例子\n[exampleSDID@32473 iut=\u0026#34;3\u0026#34; eventSource=\u0026#34;Application\u0026#34; eventID=\u0026#34;1011\u0026#34;][examplePriority@32473 class=\u0026#34;high\u0026#34;][sigSig ver=\u0026#34;1\u0026#34; rsID=\u0026#34;1234\u0026#34; ... signature=\u0026#34;...\u0026#34;] 格式就是这么个格式 \u0026lsquo;exampleSDID@32473\u0026rsquo; 是个 SD-ID,有一些标准化的 ID,比如这里的\u0026rsquo;sigSig\u0026rsquo;。\n进阶功能 接收(顾名思义) imudp imtcp imfile 处理 mmjson rfc3164 的简单 json 格式化 mmnormalize 用 liblognorm 自定义解析规则,出来是个 json 输出(顾名思义) ommysql omelastic omfile omkafka omprog: 调用自定义的处理程序,用 stdin/stdout 处理 linux 系:systemd-journald push 模型,监听/run/system/journal/syslog,日志存放/var/log/journal,配置在/etc/systemd/journald.conf\n非明文存储,利于搜索。\n结构化消息,下划线开头是 journald 自己添加的消息。用 journalctl 读取。例子:\n$ journalctl -n1 -o verbose -- Logs begin at Tue 2022-02-08 19:21:10 CST, end at Mon 2022-03-28 08:05:23 CST. -- Mon 2022-03-28 08:05:23.517238 CST [s=5a9ef8f0b27749e8be1fb46418165a0c;i=1ccd;b=1c4958a7b26240628713f627042e87f8;m=f2d223e7b;t=5db3c119c1b36;x=fc7b3dd29adfe7b8] PRIORITY=6 _UID=1000 _GID=1000 _CAP_EFFECTIVE=0 _AUDIT_SESSION=3 _AUDIT_LOGINUID=1000 _SYSTEMD_OWNER_UID=1000 _SYSTEMD_UNIT=user@1000.service _SYSTEMD_SLICE=user-1000.slice _SYSTEMD_USER_SLICE=-.slice _BOOT_ID=1c4958a7b26240628713f627042e87f8 _MACHINE_ID=2ce2d4feff754f93ab21e7355f77e4c6 _HOSTNAME=ybaipc _TRANSPORT=stdout _COMM=chrome _EXE=/snap/chromium/1945/usr/lib/chromium-browser/chrome _SELINUX_CONTEXT=snap.chromium.chromium (enforce) _SYSTEMD_CGROUP=/user.slice/user-1000.slice/user@1000.service/snap.chromium.chromium.990dd0f1-5415-48d7-b34d-0847ae198b0b.scope _SYSTEMD_USER_UNIT=snap.chromium.chromium.990dd0f1-5415-48d7-b34d-0847ae198b0b.scope _SYSTEMD_INVOCATION_ID=f24c28562e634bd98b4dcf7bbe178952 _STREAM_ID=c8846ca93bce46b7903aaa6881b310d9 SYSLOG_IDENTIFIER=chromium_chromium.desktop _PID=8770 _CMDLINE=/snap/chromium/1945/usr/lib/chromium-browser/chrome --type=utility --utility-sub-type=network.mojom.NetworkService --lang=en-US --service-sandbox-type=none --change-stack-guard-on-fork=enable --shared-files=v8_context_sna\u0026gt; MESSAGE=[8770:8773:0328/080523.517107:ERROR:ssl_client_socket_impl.cc(995)] handshake failed; returned -1, SSL error code 1, net_error -101 信息是真的很丰富了\n进阶功能 journal的进程为system-journald.service\n这也可以配置日志转发,但这里叫systemd-journal-upload.service\n接收端是systemd-journal-remote.service\n还有个可以通过http进行日志记录的systemd-journal-gatewayd.service\n但是!:没有过滤!\nelk:logstash+filebeat Pull 模型,日志系统主动拉取消息。\n本来是直接部署 logstash 的,但是资源占用堪忧(java 写的),现在直接部署 filebeat(golang 写的)只用作日志搜集。logstash 用作格式转换和 es 写入。\n存储一般就到了 elasticsearch 里,展示就用 kibana。\n进阶功能 对接 elastic 的生态,尤其是各种日志生成的 visualize+dashboard\n以及监控,很大的可玩性\n就是有点费机器\nKernel Log kernel的log是硬编码在内核里的一个ringbuffer的。一般发行版会起一个klogd的进程,不断的读取这个ringbuffer,然后发给syslogd。syslogd把消息再存到磁盘上。\nkernel的log一般就是printk出来的。\n用户空间和kernel的log相关的系统调用就2个\n syslog(就叫这个,和glibc的syslog同名) klogctl Rsyslog in action rsyslog兼容通用的配置格式,也就是$ModLoad/$WorkDirectory这种美元符号开头的脚本式格式。但后续增添了Rainer Script(作者叫Rainer)的新型配置格式,还能混着用。我看了下代码,解析这块就是传统的flex+bison写的一个解析器。这个有明确的作用域,可以用花括号包起来,写复杂逻辑推荐用这个新的格式。\n小目标 当前的某个应用里有不少流水数据,每次查询的时候需要登陆到机器上手动从日志中grep;难处在程序有几十个实例,分散在多台机器上,手动查找IP并一路cd的操作效率很低。现在希望可以将这些数据格式化的送入数据库中,后续就可以借助丰富的数据库前端或者各种BI(metabase之类)来进行查询。\n需要做的一些步骤\n 监控日志文件: 自带模块 imfile 日志提取与结构化: mmnormalize,利用liblognorm从消息中提取字段,规整化到json中,供后续步骤使用 发送到数据库: ommysql模块可以提供支持,也有ommongo/omelastic等后端 关联模块准备 数据库就MySQL了,默认这个已经足够了。\nCentos 7里默认的Rsyslog还是有点老,好在官网提供了最新的仓库,自己添加一个repo就能用(Install rsyslog on RHEL/CENTOS)。\n$ yum install rsyslog-mmnormalize rsyslog-mysql 配置文件 默认文件为/etc/rsyslog.conf,文件中大部分发行版默认配置一条$IncludeConfig /etc/rsyslog.d/*.conf,因此可以将自己的配置放在该目录下即可。这次的配置可以全部集中到/etc/rsyslog.d/myconf.conf中。\n全局使用的配置尽量放在/etc/rsyslog.conf,比如启用模块的指令,就需要放在文件的最开始。\nRainer script中,作为key的名字大小写无关。\n# /etc/rsyslog.conf module(load=\u0026quot;imfile\u0026quot;) module(load=\u0026quot;mmnormalize\u0026quot;) module(load=\u0026quot;ommysql\u0026quot;) # /etc/rsyslog.d/myconf.conf input( type=\u0026quot;imfile\u0026quot; File=\u0026quot;/tmp/mylog.txt\u0026quot; Tag=\u0026quot;mylog:\u0026quot; RuleSet=\u0026quot;rs_me\u0026quot; # 绑定的ruleset ) # 示例日志 # 2021-12-13 12:12:12.132435,Quantity: 23 template(name=\u0026quot;tpl_biz\u0026quot; type=\u0026quot;string\u0026quot; option.sql=\u0026quot;on\u0026quot; string=\u0026quot;insert into biz_table (created_at, quantity) values (\\\u0026quot;%$!gentime%\\\u0026quot;, \\\u0026quot;%$!quantity%\\\u0026quot;)\u0026quot;) # 这2个template是等价的 # template(name=\u0026quot;tpl_biz\u0026quot; type=\u0026quot;list\u0026quot;) { # constant(value=\u0026quot;insert into biz_table (quantity) values (\u0026quot;) # property(name=\u0026quot;$!gentime\u0026quot;) # constant(value=\u0026quot;, \u0026quot;) # property(name=\u0026quot;$!quantity\u0026quot;) # constant(value=\u0026quot;)\u0026quot;) # } ruleset(name=\u0026quot;rs_me\u0026quot;) { action(type=\u0026quot;mmnormalize\u0026quot; rulebase=\u0026quot;/etc/rsyslog.d/me.rulebase\u0026quot; ) if $!event.tags contains \u0026quot;qty\u0026quot; then { # 这里的tags是从rulebase里来的 action(type=\u0026quot;ommysql\u0026quot; Template=\u0026quot;tpl_biz\u0026quot; server=\u0026quot;localhost\u0026quot; serverport=\u0026quot;3306\u0026quot; db=\u0026quot;Syslog\u0026quot; uid=\u0026quot;aaa\u0026quot; pwd=\u0026quot;aaa\u0026quot; ) } else { action(type=\u0026quot;omfile\u0026quot; File=\u0026quot;/tmp/me.out\u0026quot; ) } } 日志文件监听 数据源需要用input来指明,tag属性是必需的。\n格式解析:liblognorm mmnormalize只是简单调用了一下lognormalizer的功能。lognormalizer可以先从shell里调用来确认自己写的规则是不是正确。\n举例,现在的rulebase是这样的\n# me.rulebase rule=qty:%gentime:char-sep:,%,Quantity: %quantity:number% 需要这样调用,默认输出是json,-T输出event.tags。\n$ echo \u0026#39;2021-12-13 12:12:12.132435,Quantity: 23\u0026#39; | lognormalizer -r me.rulebase -T { \u0026#34;quantity\u0026#34;: \u0026#34;23\u0026#34;, \u0026#34;gentime\u0026#34;: \u0026#34;2021-12-13 12:12:12.132435\u0026#34;, \u0026#34;event.tags\u0026#34;: [ \u0026#34;qty\u0026#34; ] } 格式解析的模块是mmnormalize,是绑定的ruleset:rs_me的一部分。\n注意: 在centos7上默认的rsyslog版本,如果rule里的key是大写开头(quantity-\u0026gt;Quantity),有概率是没有办法在action的template里去到这个值。所以尽量把抽取出来的key按照全部小写命名。\n输出到数据库 时间格式 为了能直接送到db里,时间格式需要调整到MySQL直接识别的格式,也就是YYYY-MM-DD hh:mm:ss[.fraction] 若可以直接调整代码是最好不过了 时间精度 用DATETIME(6)来定义,可以精确到微妙(10^-6)。这里的6就是fraction。 需要事先在db中新建一张表\nCREATE TABLE `biz_table` ( `id` bigint unsigned NOT NULL AUTO_INCREMENT, `created_at` datetime(6) DEFAULT NULL, `quantity` bigint DEFAULT \u0026#39;0\u0026#39;, PRIMARY KEY (`id`), ); template里option.sql=\u0026quot;on\u0026quot;是必须的,可能是出于编译模板的需要。\n排错指南 rulebase需要事先在shell里验证好 module的加载放在/etc/rsyslog.conf开头 发行版默认是用的systemd,写好配置后用systemctl restart rsyslog重启,然后systemctl status rsyslog看下启动的输出。这个输出只有几行,如果出错的地方很多,可以用journalctl -u syslog调用默认pager(也就是less)看完整的启动日志。 开发相关 不找依赖的话,估计现在没有开箱即用的 rfc5424。glibc 的 syslog 还是太弱了点。\nglibc 里 syslog 相关的实现 openlog(tag, flag)\ncloselog()\nsyslog(level, fmt, \u0026hellip;)\n代码在glibc/misc/syslog.c里,不复杂,但是只能 Log 到\u0026rdquo;/dev/log\u0026quot;里。不过倒是线程安全的。\n这个是符合 rfc3164 的\nsystemd service 如果包装成systemd service,用systemd接管程序的起停,那么自动获得送入journald的功能。\nsd-journal 开发时依赖一个 systemd 的库 libsystemd-dev,运行时只需要一个libsystemd.so就行了。\nsystemd for Developers III (点进去看例子)\n只需要几个函数\n sd_journal_send 可自己添加kv sd_journal_print 类似 syslog 如何用到实际项目里 需要自己包装一个吗?\npush/pull区别在哪儿?\n 开发、运维 流量 结构化在哪儿做(生产者机器还是集中消费的地方) 想法:日志记录时进行标准化的结构,直接落es库。\n落es之前的步骤如何选择?\n"},{"id":1,"href":"/docs/cpp/traits/","title":"【翻译】Traits:一种新的而且有用的Template技巧","section":"Docs","content":" 文章来自Traits: a new and useful template technique\n应该是很老(1995)的文章了,不过很适合作为Template入门的材料。\n ANSI/ISO C++标准库一开始就想支持国际化(internationalization),虽然一开始还没相好具体细节,但是最近5年逐渐有点眉目了。现在得到的结论是,应当用template来对需要进行字符操作的工具进行参数化。\n给现有的iostream和string类型进行参数化其实挺难的,需要发明一种新的技术才行。幸运的是,这种技术可以很好的服用在其他地方。\n问题 在iostream中,streambuf需要一个EOF来完成自身的功能。在一般的库中,EOF就是一个int而已;一些需要读取字符的函数也只返回int数值。\nclass streambuf { ... int sgetc(); // return the next character, or EOF. int sgetn(char*, int N); // get N characters. }; 但如果用一种字符类型来参数化streambuf,会发生什么?一般来说,程序并不需要这个类型,而是需要这个类型的EOF的值。先试试看:\ntemplate \u0026lt;class charT, class intT\u0026gt; class basic_streambuf { ... intT sgetc(); int sgetn(charT*, int N); }; 这个多出来的intT就有点烦人了。库的使用方才不关心所谓的EOF是啥。问题还不只这个,sgetc在遇到EOF时应该返回什么?莫非要再来一个模板参数?看来这种方式行不通。\nTraits技法 这时候就需要引入Traits了。有了traits,就不需要从原来的模板里直接取参数,而是定义一个新的模板。用户一般不会直接使用这个新的模板,所以这个模板名字可以起的人性化一点。\ntemplate \u0026lt;class charT\u0026gt; struct ios_char_traits { }; 默认的traits是个空类。对于真实的字符类型,可以特化这个模板以提供有意义的语义。\nstruct ios_char_traits\u0026lt;char\u0026gt; { typedef char char_type; typedef int int_type; static inline int_type eof() { return EOF; } }; ios_char_traits并没有数据成员,只有一些定义。再看看streambuf该怎么定义。\ntemplate \u0026lt;class charT\u0026gt; class basic_streambuf { public: typedef ios_char_traits\u0026lt;charT\u0026gt; traits_type; typedef traits_type::int_type int_type; int_type eof() { return traits_type::eof(); } ... int_type sgetc(); int sgetn(charT*, int N); }; 除去typedef,这个和最开始的定义很像。现在就有1个模板参数,也是用户需要关心的唯一一个模板参数。编译器会从这个trait类中寻找需要的信息。除了一些变量的声明需要调整,用户的使用代码和之前看起来没有太大不同。\n如果streambuf用了另外一个字符类型,这时只要重新特化ios_char_traits即可。要支持wchar_t可以这么写:\nstruct ios_char_traits\u0026lt;wchar_t\u0026gt; { typedef wchar_t char_type; typedef wint_t int_type; static inline int_type eof() { return WEOF; } }; string类可以用同样的方式参数化。\n这个技巧的适用场景:1. 对原始类型进行模板参数化;2.对没办法改动的类进行定制\n另一个例子 更进一步解释之前,再看看另一个例子(来自ANSI/ISO C++ [Draft] Standard)。\n有一个数值计算库使用的类型有float, double和long double,每个类型都有关联的\u0026quot;epsilon\u0026rdquo;、指数和底数。这些数值在\u0026lt;float.h\u0026gt;里都有定义,但库中有一些工具不知道改用FLT_MAX_EXP还是DBL_MAX_EXP。用traits技术可以很干净的解决这个问题。\ntemplate \u0026lt;class numT\u0026gt; struct float_traits { }; struct float_traits\u0026lt;float\u0026gt; { typedef float float_type; enum { max_exponent = FLT_MAX_EXP }; static inline float_type epsilon() { return FLT_EPSILON; } ... }; struct float_traits\u0026lt;double\u0026gt; { typedef double float_type; enum { max_exponent = DBL_MAX_EXP }; static inline float_type epsilon() { return DBL_EPSILON; } ... }; 现在可以在不知道具体类型(float/double/long double)直接取到max_exponent。举个matrix的例子\ntemplate \u0026lt;class numT\u0026gt; class matrix { public: typedef numT num_type; typedef float_traits\u0026lt;num_type\u0026gt; traits_type; inline num_type epsilon() { return traits_type::epsilon(); } ... }; 到现在为止的例子里,每个模板参数都有一系列public的typedef,而使用这些参数的类都强依赖于这些typedef。这绝非偶然:大多数的情况下,作为参数的traits必须提供public的typedef,使用这些traits的template才能正确的实例化。\n学到一点:一定要提供这些public的typedef。\n默认模板参数 到1993年为止,编译器就可以支持上述的用法。1993年11月后,一个更好的方案呼之欲出:可以制定默认的模板参数。当下已经有不少编译器支持数值作为默认模板参数了,新方案更进一步,允许类型作为默认模板参数。\nStroustrup\u0026rsquo;s Design and Evolution of C++ (page 359)有一个示例。首先定义一个traits: CMP,和一个简单的参数化的string。\ntemplate \u0026lt;class T\u0026gt; class CMP { static bool eq(T a, T b) { return a == b; } static bool lt(T a, T b) { return a \u0026lt; b; } }; template \u0026lt;class charT\u0026gt; class basic_string; 这时就可以为string自定义compare操作了:\ntemplate \u0026lt;class charT, class C = CMP\u0026lt;charT\u0026gt; \u0026gt; int compare(const basic_string\u0026lt;charT\u0026gt;\u0026amp;, const basic_string\u0026lt;charT\u0026gt;\u0026amp;); 这里不讨论具体实现细节,但需要关注第二个模板参数。首先,这个C不仅仅是class,而且是实例化后的class。其次,第二个模板参数(C)自己也需要参数,而需要的参数是compare的第一个模板参数(charT)。这在函数声明中是不可以的,但在模板声明时可行。\n这种方式允许用户可以自定义比较的过程。把这个技术应用在我们自己的streambuf模板上看下:\ntemplate \u0026lt;class charT, class traits = ios_char_traits\u0026lt;charT\u0026gt; \u0026gt; class basic_streambuf { public: typedef traits traits_type; typedef traits_type::int_type int_type; int_type eof() { return traits_type::eof(); } ... int_type sgetc(); int sgetn(charT*, int N); }; 这给了我们为特定char定制traits的机会。这对库的用户来说很重要,因为EOF在不同的字符集映射中是有可能不一样的。\n运行时的Traits 更进一步,看戏streambuf的构造函数:\ntemplate \u0026lt;class charT, class traits = ios_char_traits\u0026lt;charT\u0026gt; \u0026gt; class basic_streambuf { traits traits_; // member data ... public: basic_streambuf(const traits\u0026amp; b = traits()) : traits_(b) { ... } int_type eof() { return traits_.eof(); } }; 现在我们traits也可以在运行时发挥作用了,而不仅仅是编译时。在这个例子中,traits_.eof()可能是一个静态函数,或者是一个普通的成员函数。如果是普通成员函数,eof()可能会用到构造traits时的一些参数。(这个技巧是有实际使用场景的,比如标准库里的容器都有的allocator)\n值得注意的是,对使用方来说,现在没有任何的改变,默认参数可以满足大部分的使用需求。但是当有自己特殊需求时,现在的模板定义也能提供修改的机会。不论什么情况下,我们都会生成最优的代码,如果不需要额外的代价,我们就不会引入这些额外的代价!\n总结 只要编译器支持template,traits技巧就可以直接上手用起来了。\nTraits可以将相关联的类型、值、函数等用模板参数关联起来,同时不引入过多的噪声。这项语言特性(默认模板参数)极大的扩充了语言能力,提供了足够的灵活性,也丝毫不损害运行效率。\n参考 Stroustrup, B. Design and Evolution of C++, Addison-Wesley, Reading, MA, 1993. Barton, J.J., and L.R. Nackman, Scientific and Engineering C++, Addison-Wesley, Reading, MA, 1994. Veldhuizen, T. \u0026quot; Expression Templates\u0026rdquo;, C++ Report, June 1995, SIGS Publications, New York. "},{"id":2,"href":"/docs/cpp/cpp_emptyclass_opt/","title":"【翻译】c++类中“空成员”的优化","section":"Docs","content":"本文来自于The \u0026ldquo;Empty Member\u0026rdquo; C++ Optimization。是我在看c++ std::string代码时遇到的一个链接,其中解释了为什么_Alloc_hider会采用inhert from Alloc的原因。\n文章应该是97年的,所以里面的指针长度还是4 byte。\nc++类中“空成员”的优化 C++标准库中有很多有用的模板,包括享誉盛名的SGI STL。这些模板的实现很高效,也不失灵活。在日常的编程中,可以把这些模板当作范例来进行学习,也可启发我们如何进行兼顾灵活性与效率的程序设计。\n“空成员”的优化,就是这样的一个典范:一个没有类成员的class,就不应该占用内存空间。什么情况下需要一个没有类成员的class呢?这样的class一般会拥有一系列的typedef或者成员函数,而程序的调用方可以用自己定义的类似的class来完成一些特殊的功能(自定义的class可不一定没有类成员)。这个默认提供的class应该可以满足绝大多数的需求。这种情况下,优化这个空成员的class是个很有性价比的事情。\n由于语言的限制(之后会解释),空成员的class通常会占据一定的内存空间。如果是一般情况也就算了,但是在stl里,不进行优化的话,还是会劝退很多潜在的使用者的。\n空成员“膨胀” 以STL举例。每个STL的容器都有一个allocator的参数,当容器需要内存的时候,它会向allocator去申请。如果用户想要自己定制化内存申请过程,那么就可以在构造容器时提供自己的allocator。大多数情况下,容器用的是STL默认的allocator,这个默认的allocator直接调用new来完成分配。这是个空类,类似于下面这个定义\ntemplate \u0026lt;class T\u0026gt; class allocator { // an empty class . . . static T* allocate(size_t n) { return (T*) ::operator new(n * sizeof T); } . . . }; 举个list的例子,class list保存了一个私有的allocator成员,这个成员在构造函数里进行赋值\ntemplate \u0026lt;class T, class Alloc = allocator\u0026lt;T\u0026gt; \u0026gt; class list { . . . Alloc alloc_; struct Node { . . . }; Node* head_; public: explicit list(Alloc const\u0026amp; a = Alloc()) : alloc_(a) { . . . } . . . }; 成员list\u0026lt;\u0026gt;::alloc_通常占据4 byte,尽管这个Alloc是个空类。这通常来说不太会是个问题。但万一list自身是一个巨大的数据结构的一个节点(比如vector),当vector很大的时候,这种额外的空间消耗是不可忽视的。巨大的内存占用意味着更慢的执行速度。就算在当下,相对于cpu自身的频率来说,内存访问已经非常慢了。\n空对象 那么改怎么解决这个问题?解决问题之前,首先需要搞清楚为什么这里会有这一层开销。C++的语言定义是这么说的:\n A class with an empty sequence of members and base class objects is an empty class. Complete objects and member subobjects of an empty class type shall have nonzero size.\n 空类:没有数据成员,并且没有基类。这个基类实例化出来的完整对象的大小不应该为0.\n 解释以下这个规定的缘由:\nstruct Bar { }; struct Foo { struct Bar a[2]; struct Bar b; }; Foo f; 那么f.b和f.a[]分别是什么?如果sizeof(Bar)是0,那么这2个地址就是一样的。如果你用地址来作为对象的标识,那么f.b和f.a[0]就是同一个对象。C++标准委员会通过禁止空类的对象大小为0来解决这个问题。\n但为什么还需要占据4 byte的大小呢?虽然大部分的编译器认为sizeof(Bar) == 1,但4 byte是对象对齐的需求。比如:\nstruct Baz { Bar b; int* p; }; 结构体Baz在大多数的体系结构上大小是8 byte,编译器自己在Baz::b后面添加补齐,是为了让Baz::p不会横跨一个字(word)。\n struct Baz +-----------------------------------+ | +-------+-------+-------+-------+ | | | Bar b | XXXXX | XXXXX | XXXXX | | | +-------+-------+-------+-------+ | | +-------------------------------+ | | | int* p | | | +-------------------------------+ | +-----------------------------------+ 那该如何规避调这个额外的开销呢?C++标准也在Footnote里提了一嘴:\n A base class subobject of an empty class type may have zero size. 空类作为基类时,其大小可以为0\n 也就是说,如果是这个结构体\n struct Baz2 : Bar { int* p; }; 编译器就会认为Bar的大小为0,这样sizeof(Baz2)就是4。\n struct Baz2 +-----------------------------------+ | +-------------------------------+ | | | int* p | | | +-------------------------------+ | +-----------------------------------+ 编译器并未要求实现成这个样子,但是你可以认为大部分标准的编译器就是这样实现的。\n消除膨胀 现在你知道了消除这个开销的原理,问题是接下来怎么做?最直观的,list\u0026lt;\u0026gt;直接继承Allocator,如下:\ntemplate \u0026lt;class T, class Alloc = allocator\u0026lt;T\u0026gt; \u0026gt; class list : private Alloc { . . . struct Node { . . . }; Node* head_; public: explicit list(Alloc const\u0026amp; a = Alloc()) : Alloc(a) { . . . } . . . }; 这当然是可以的。list里的成员函数可以直接调用this-\u0026gt;allocate(),而非allco_.allocate()完成内存申请。 不过,用户提供的Alloc是允许拥有虚函数的,这可能会和子类list\u0026lt;\u0026gt;里的某些方法有冲突。(list\u0026lt;\u0026gt;::init和Alloc::init())。\n另一种可行的方式是,将Alloc打包到list\u0026lt;\u0026gt;的成员变量上(比如指向第一个list node的指针),这样Allocator的接口不会暴露出来。\ntemplate \u0026lt;class T, class Alloc = allocator\u0026lt;T\u0026gt; \u0026gt; class list { . . . struct Node { . . . }; struct P : public Alloc { P(Alloc const\u0026amp; a) : Alloc(a), p(0) { } Node* p; }; P head_; public: explicit list(Alloc const\u0026amp; a = Alloc()) : head_(a) { . . . } . . . }; 采用这种方法实现的话,申请内存就用head.allocate()。没有额外的开销,list\u0026lt;\u0026gt;用起来也和以前一样。不过就像其他做的好的优化一样,实现上总是有点丑陋,但总归不会影响到接口了。\n统一一点的解决方案 当然还有提升的空间。看看下面这个template\ntemplate \u0026lt;class Base, class Member\u0026gt; struct BaseOpt : Base { Member m; BaseOpt(Base const\u0026amp; b, Member const\u0026amp; mem) : Base(b), m(mem) { } }; 用这个模板,那么list的接口就可以变成这样:\ntemplate \u0026lt;class T, class Alloc = allocator\u0026lt;T\u0026gt; \u0026gt; class list { . . . struct Node { . . . }; BaseOpt\u0026lt;Alloc,Node*\u0026gt; head_; public: explicit list(Alloc const\u0026amp; a = Alloc()) : head_(a,0) { . . . } . . . }; 这个实现相比与最开始的版本,看起来也没那么不堪。其他的STL容器,也可以借助BaseOpt来简化代码。只不过,成员函数申请内存的时候就会奇怪一些,这个我们现在还暂时不考虑。\n现在可以在BaseOpt定义的地方加上很好的文档来描述这个优化技术了。\n也可以在BaseOpt里添加一些成员,但是并不建议这样做。这可能会造成和Base里名称冲突(就像我们在第一次尝试消除膨胀的代码里一样)。\n收尾 这项技术可以在当下通用的编译器里使用。就算编译器没有实现空基类优化,也没有额外的开销。\n"},{"id":3,"href":"/docs/osc/systemd_read/","title":"Systemd阅读笔记","section":"Docs","content":"Systemd的争议挺大的,不来看看大家为什么抵制它吗?\n需要理顺数据结构,再抓关键流程。\nReading Date: 20201211 Fri 20201212 Sat 20201213 Sun 集中的3天看,理顺了基本的框架 Overview # commit 5916c54a8dabd25efa0d78deef78fbfab684b8a4 No filename linenum 1 execute.c 6514 2 unit.c 6367 3 load-fragment.c 5851 4 manager.c 4978 5 service.c 4587 6 cgroup.c 3778 7 socket.c 3517 8 dbus-execute.c 3459 9 dbus-manager.c 3317 10 main.c 2930 11 dbus-unit.c 2475 12 namespace.c 2384 13 mount.c 2204 14 dbus-cgroup.c 1718 15 job.c 1698 16 swap.c 1690 17 dbus.c 1250 18 transaction.c 1201 19 automount.c 1129 20 device.c 1120 21 timer.c 956 拉取了src/core/下代码最多的几个文件。从这个大致能看出来复杂度集中在哪些方面,作为调度各类进程运行状态的一个程序,execute相关的复杂度最大当之无愧,控制进程执行会涉及到方方面面,这也是systemd不愿意跨系统移植的最大阻力吧。\nunit/service/socket/mount/device/timer/automount/swap这些东西是一类,是作为一种entry而存在,是被动、被调用、被使用的实体。\nmanager/job/transaction/load-fratment是内阁,作为进程驱动的存在。\ndbus则是暴露了内部功能给其他进程使用,作为接待处,要负责请求命令的转换,所以归为一块相对独立的功能。\nData Structure Manager 如其名所示,Manager是整个systemd的核心所在。这是一个巨大的结构体。\n在运行时,主循环就在做一件事,dispatch各种queue。这些个queue都是附着在manager上的,比如\n load queue\n加载解析unit file到内存中\n run queue\njob存在的地方,处理本职工作的地方\n gc job\n gc unit\n cleanup\n cgroup realize\n dbus queue\n Unit 每个UnitType都有自己的UnitVTable注册到全局,UnitVTable用于操作Unit。这也是Manager用于控制各类unit的手段。Manager不关心具体的每个unit该怎么操作,它只需要关注自己需要那些操作行为,然后定义到vtable里,由各类unit自己去实现。\nJob 需要关注Job状态在何时进行转换。\nComponents Systemd开发组在开发的过程中逐渐抽象出来的组件,对于项目级别来说是很好用的。这些代码都放在src/libsystemd下面,头文件则在src/systemd。\nsd-event 类似于GIO,封装了自己的eventloop;socketfd、timer、signal都作为一个event source,可以绑定自己的处理函数上去。\nsd-bus Systemd自己的DBus实现。\nStartup Procedure and main loop 大量使用了查表,是个查表狂魔项目。\n 静心想想一个init程序要干什么,它的运行时表现是什么?\n 开机的时候要启动各种程序 其他程序跑出来的zombie进程要收割 开机后用户也会起进服务(daemon),现在这个工作依托给了systemd来完成。 systemd其实也是一个server程序,只不过和这个server交互的方式比较不一样,不是通过tcp,虽然它也支持,但主要还是dbus的那个unix socket,外加各种信号。而通过sd-event的统一,这些交互都化身为sd-event-source,可以被epoll监听。为什么说systemd是linux only,可能也是得益于linux上才有signalfd/eventfd/timerfd吧。\nStartup 启动是最复杂的。作为一个要兜底的进程,需要考虑到各种婆婆妈妈的情况。\n在代码里写死的启动项目,叫SPECIAL_DEFAULT_TARGET \u0026quot;default.target\u0026quot;。之后之所以能带动起到了的各种unit的启动,是因为有systemd的unit加载启动机制。还好项目的代码写的很明了,对于启动需要关注一个函数就行:manager_startup。\n 找到default.target在哪里,这里的寻找过程就奠定了使用时该怎么找启动文件,很冗长也很无聊的代码 systemd也支持旧式的initrc脚本,总要有个识别旧式启动方式的过程,所以接下来就是这个识别并收为己用的过程。systemd把这个过程命名为generator。 接下来的过程叫做enumerate_perpetual/enumerate,用于一些mount的对象,可以发现当前系统里存在的设备。 dbus也要在这个时候做好准备。 把SPECIAL_DEFAULT_TARGET加入到load queue中。这个算是比较标志性的一个点了。可以说systemd真正的启动过程就是从这里来的。关键函数是do_queue_default_job。 进入sd-event的循环,正式开始上班。 do_queue_default_job 把default.target加载到内存里,并连带依赖一起加载的工作是在manager_load_startable_unit_or_warn中进行的。这里比较关键的接口是manager_load_unit。这里分2步走\n manager_load_unit_prepare : 找到这个目的unit,初始化,然后加入到manager的load_queue中。 manager_dispatch_load_queue : 循环加载就在这个里面实现。里面有一个while,从m-\u0026gt;load_queue中不断取unit来加载。虽然刚开始只加入了一个default.target,但是加载default.target的时候会不断的把所依赖的unit插入到m-\u0026gt;load_queue之中。循环往复,直到所有依赖都加载完成。 这时这些unit终于从文件变成了内存的结构,准备工作完成。接下来启动的工作由manager_add_job完成。\nint manager_add_job( Manager *m, JobType type, Unit *unit, JobMode mode, Set *affected_jobs, sd_bus_error *error, Job **ret) r = manager_add_job(m, JOB_START, target, JOB_ISOLATE, NULL, \u0026amp;error, \u0026amp;job); 看签名的话,manager_add_job会返回一个job给我们,但是job却不是这个函数里分配的。函数里用了一个临时的Transaction来处理依赖,在transaction里有一个anchor_job,就是我们所请求的起始job。函数最终返回的就是这个anchor_job。\n"},{"id":4,"href":"/about/","title":"关于","section":"","content":"思绪随风飘扬。\n contract me aiueclipse@mail.com\n "},{"id":5,"href":"/docs/myth/thinking_in_multithread/","title":"多线程交互的思考","section":"Docs","content":"最近开始写多线程的程序,整理一下对多线程代码的思考。\n用多线程的时机 多线程为任务划分提供了天然的要求。如果能抽象出来流水线的业务模型,便有了多线程的用武之地。\n线程间交互 虽说操作系统或、C库或者语言给应用层提供了多种线程交互的方式,包括但不限于锁、信号量,但线程之间最好还是不要有什么交互,这样才是最安全的。\n退而求其次,多个线程之间只有一个交互点:无锁队列,这种编程模型交互很清晰,也不容易出错。一个好的无锁fifo实现是必要的。 多线程天生适合这种pipeline的方式处理数据。\n无锁队列 无锁队列只需要关注memory order就能实现交互,犯不着把进程切出去,独占CPU。看着好浪费呀。确实,对于time critical的应用,只能busy loop。\n无锁队列也有很多种,从使用上来看,队列里放定长数据还是不定长数据,其实现方法是完全不一样的。定长item的要简单一点,in/out节点可以用2个atomic来指代下标就可以了。不定长的实现我现在还没有见到过,但不定长的话,怎么解决回绕的问题,想必也是有点头疼的。\n为了避免思考回绕怎怎么解决,我倒是想到了可以采用指针+memory pool的方式,这对内存管理又提出了很高的要求。\n所以还是结合业务来看,如果能接受一定量的空间冗余,把自己的msg都包到一个union里,这倒是不错的选择。要知道,定长的也不一定慢,我自己在机器上测过一些数据,找个时间系统的来重新测试一遍。\n多线程下的内存管理 pipeline工作模式下内存管理有一个思路,就是线程间只传递指针,具体的数据结构在最开始的线程中进行分配(可以用pool的方式来优化),也只能在最开始的线程进行释放。线程间有2个queue,前向+后向。每个线程用完后决定继续往后面传递还是回传到上一个线程。增加了一点程序编写的复杂度,换来对内存管理的简化。\n日志 多线程框架下,日志也是需要考量的一个重要组件。对C++来说,根据编译条件开启、关闭部分代码可以达到加速的效果,虽然很微小,但还是要做。日志就是属于这样的一种代码。\n除了boost.log/spdlog/glog等比较老派的日志库,后面出现的NanoLog算是开辟了新纪元,二进制日志保证了吞吐,强类型日志保证了速度。但是format本身的确是一个耗时的操作,实际操作中打日志一般会放在单独的线程中进行。\n网络栈 网络协议本身只是一种约定而已,搞明白协议要干啥后,实现协议也只是一种相对简单的体力活。\n比如,抓包要用packet socket,如果protocol指定为0,那么在应用层是不会收到任何包的;这个在man page里没有提及,也是在看内核的时候想到的。 可以用mmap的方式来抓包,这也是libpcap用的方法。这居然还对应着不同的版本,每个版本用的内存模型和header结构也不一样。在看这些代码的时候,仿佛看到了各个大公司里的某些个小人物,也就几个人吧,推动着世界上某个重要东西的发展。我能想象到一个黄昏的下午,一个程序员改好代码,写好文档,躺下休息的情况。在这个场景后,世界上的抓包程序的写法就定了,造物主也无法再改变什么。\n想要更高的性能,就需要绕过kernel,从网卡上自己收。反正也是要低延时了,抛开一切自己搞,while true receive frame,送到队列里交给下个线程处理。市面上常见的方法有DPDK,主要对接的是Intel自己的网卡。还有一些其他的专门做低延迟的网卡,比如solarflare和exanic,有自己单独的lib和kernel module。\n"},{"id":6,"href":"/docs/osc/waf/","title":"Waf - a Python based build system","section":"Docs","content":"看懂这篇文章需要一点使用waf的经验,不过也不费事,看看例子也够了。\n构建系统简谈 软件构建系统不像是个很多人在研究的东西,所以在网络上很少能找到剖析某个构建系统原理、或者阐述构建系统principle的文章。看ns3的过程中接触到了waf,发现其文档waf book很好的阐述了构建系统的一些基础知识,个人认为比cmake的文档好一些。因为其核心只有十几个文件,这个构建系统只需要一个10k+的waf文件,所以可以放到版本库里(像对python的评价一样,batteries included),唯一要求就是环境中有python,而这对一个开发人员来说显然不是一件困难的事情。\n|-- Build.py |-- ConfigSet.py |-- Configure.py |-- Context.py |-- Errors.py |-- Logs.py |-- Node.py |-- Options.py |-- Runner.py |-- Scripting.py |-- Task.py |-- TaskGen.py |-- Tools [directory] |-- Utils.py |-- ansiterm.py |-- extras |-- fixpy2.py `-- processor.py 以上便是所有waf的内容,可以看到涉及到的文件不算多。Tools下包含了很多语言的构建工具,比如c/c++/java/qt/ruby/tex等等,如果自己有能力定制,可以只保留自己项目里需要的tool,可以做到更小。(虽然个人认为没有必要)\n核心抽象 如果是写编译语言的(c/c++/rust/go/fc/d),那么构建系统是每天都在用的。在敲击make之后,屏幕上出现了一系列的自动运行的命令,然后就是漫长的等待。用waf也一样,一般是./waf configure build clean dist\u0026hellip;再等机器的轰鸣停止后继续工作流。waf提供了一些核心的抽象,从而能够表达出构建这个活动的几个关键方面:\n 像make clean dist类似,可以在构建命令后面自行添加指令,这种capibility由Context提供 构建系统最重要的功能就是按需构建,要判断出哪些文件要编译而哪些是不用的,这用到了TaskGen与Task的抽象 并行构建提升速度,由Runner来提供。 这3个抽象几乎相互独立,个人认为是很好的一个抽象。\nContext 每一个跟在./waf后面的指令,都对应一个Context。如果是build/configure/list/step/install/uninstall,waf自行提供了对应的Context的子类用于执行这些命令,如果是其他的自定义函数,那么就会依托于Context本身,可以在自定义函数里用Context自定义的函数,比如recurse来遍历子目录执行子目录里的同名自定义函数。\n如果项目根目录下的wscript有do_sth,就可以./waf do_sth\ndef do_sth(ctx): ctx.load(\u0026#39;compiler_cxx\u0026#39;) # 加载工具 ctx.recurse([\u0026#39;src\u0026#39;,\u0026#39;dep\u0026#39;]) # 遍历子目录,执行子目录下wscript里的do_sth ctx.exec_command(\u0026#39;touch foo.txt\u0026#39;) ctx.msg(\u0026#39;hello\u0026#39;) 这里函数参数ctx就是指向了Context的一个实例,而do_sth是作为Context上的一个方法而存在的,可以直观的理解为,我们为Context增加了一个自定义的do_sth方法,所以可以自由调用Context里本来提供的方法。\n./waf build执行时绑定的Context是BuildConetxt,在Build.py里被定义,在waf build的时候,执行的是wscript里def build(bld)这个方法。举一个例子\ndef configure(conf): conf.load(\u0026#39;compiler_cxx\u0026#39;) def build(bld): bld.shlib(source=\u0026#39;a.cpp\u0026#39;, target=\u0026#39;mylib3\u0026#39;) bld.program(source=\u0026#39;main.cpp\u0026#39;, target=\u0026#39;app\u0026#39;, use=\u0026#39;mylib\u0026#39;) bld.stlib(target=\u0026#39;foo\u0026#39;, source=\u0026#39;b.cpp\u0026#39;) # 直接调用bld bld(features = \u0026#39;c cprogram glib2\u0026#39;, use = \u0026#39;GLIB GIO GOBJECT\u0026#39;, source = \u0026#39;main.c org.glib2.test.gresource.xml\u0026#39;, target = \u0026#39;gsettings-test\u0026#39;) 这里bld指向了BuildContext的一个实例,这意味着BuildContext里所有的方法都在这个函数里都是可用的,可以通过bld.xxx来调用。\n值得注意的是,在Build.py中,可是找不到shlib/probram/stlib这3个方法的,但是在这里却调用成功没有报错,这全部依赖于conf.load('compiler_cxx')这一句。执行这句话后,就给bld指向的BuildContext实例绑定了shlib/program/stlib这3个方法。 那直接调用bld()呢?这个就要看Build.py里的BuildContex():__call__方法了。从这里开始,就涉及到TaskGen这个抽象了。\nTaskGen \u0026amp; Task 最终需要执行的编译指令、中间代码生成等,每一条都对应一个task,我们不可能去一个一个的写task,而是希望以一种声明式的方法表达想要做的事情,这就是task_gen所完成的任务。从声明式表达到生成task的这项任务,由waf build完成。在执行的过程中,会对搜集到的每个task_gen执行一下post(),然后这个task_gen就生成了自己所有的task。作为一个灵活的构建系统,waf提供了很多方法来让我们hook到post()的过程中。对于每个task,到底该不该执行需不需要执行,它自己会追踪自己的依赖,职责分离,我很喜欢这个设计思路。\n以前一小节为例,共在build(bld)里一共进行了4次调用,这意味着生成了4个task_gen的实例,在真正执行构建过程之前,会有一个地方对这4个实例各自调用一下post(),把所有的task_gen都消灭掉,变成task。至于怎么hook,这是个比较关键的点,如果理解了,就能很好的自定义waf了。\n首先看看写好的wscript,它的声明式体现在什么地方呢?体现在函数参数里。得益于python的语言特点,可以随便加参数,然后在函数实现里用**kw来取这些值。这意味着可以随便加自己想要的key=value进去,这些加进去的参数是可以在自定义的hook过程中取到的,这算是可自定义的一个基础。(ruby自定义的能力更强,毕竟dsl是其强项,但可能限于ruby的流行程度以及发行版是否默认安装,让作者最后选择了python,不过也已经够用了)\n在post()的过程中,会从task_gen.meths[]里依次取出方法来执行,hook的方式就是把自定义的方法塞到这个task_gen.meths[]之中。这只要在自定义的方法上加一个@TaskGen.taskgen_method的注解就能实现,还是挺简洁的吧?声明式中写的key=val,都能通过taskgen.key取到,这样一来,几乎就获得了无限的能力来自定义构建过程了。\n在taskgen.meths[]里有几项预定义的方法,waf也提供了指令来让我们定制自己方法执行的位置。总而言之,想要什么内容,直接在wscript里以key=val的方式指定,然后在自己的方法里用getattr来取就行了。\n这也只是个支持性框架,具体到某个语言(c/c++)是怎么做的,到后面再看。\nRunner waf自己会默认起和cpu core相同数量的进程来执行构建认任务,而且构建过程的输出也很清晰漂亮。waf也提供了lazy的模式,不是一下子把所有的task_gen都转化,所以也是用了一些技巧来达成这个目的。在看waf代码的过程中,能看到很多pythonic和近乎炫技的技法,可见作者真是把python语言玩弄于股掌之中。\n如何实现make -j的效果?答案是Semphore,这里的job control是由几个类相互交互完成的。虽说Python的线程是鸡肋,但完成任务分派还是绰绰有余。这里分三类线程:\n 主线程,只有1个,即敲回车后生成的Python进程,其中负责交互的类为Parallel。 分派线程,也只有1个,叫Spawner,与Parallel互相引用。主线程决定了并行数量,然后在分派线程里初始化一个对应数量的Semphore。 工作线程Consumer,有一个Task,就得起1个Consumer 其实到现在的位置,要执行的Task已经都放在一个队列ready里了。遍历这个队列,acquire semphore,开新的Consume执行Task。如果Semphore用完了,那么遍历的过程就阻塞,直到Task执行结束后Consumer再把这个Semphore加回去。\nConsume从ready队列里获取任务执行,结束后放回out队列里。主线程在一个循环里从out往回拿任务,看看对不对,然后做一些统计或者直接结束构建。\n这里提到的所有类,都在Runner.py里。\nConsumer里调用的方法最后都会走到Utils.run_regular_process里,通过subprocess.Popen来完成真正的命令调用。\nC++的构建 前面的核心抽象确实相当抽象,只是提供了一种框架来执行并行执行一些任务,关于构建本身则没有任何的提及。至于如何用这种工具做到构建C++工程,则并不是一件容易的事情。\ncmake社区近些年发起了轰轰烈烈的modern cmake的运动,即迁移到target based的构建描述,而非原先支持的流水帐构建。反观waf自带的C++构建方式,天然就是target based,只不过在waf的范畴里,这个叫task generator。\nC++代码最终的产出是什么呢?有3种:\n 可执行程序 静态库 动态库 有的C++程序其实是作为其他程序的依赖而存在,典型的比如各种libssl-dev。这种类型的产出不仅只有可加载的二进制,而且还要给其他库提供编译支持,即头文件。\n有人喜欢写all in one的代码,典型的比如Fabrice Bellard写quickjs,一个文件搞定。这种代码,其实并不太需要构建系统,几行shell脚本就全都搞定,反正每次都要重新编译。不过普通人还是选普通配置,该分模块就分模块,老老实实的一个一个module去完成功能。减少构建的时间,减少重复编译的工作,这就需要构建系统的辅助,来找出哪些需要重新编译而那些可以复用。\n可以从2个角度来思考C++的构建\n 找出来哪些需要重新构建,这个工作叫依赖管理 每个构建应该怎么完成 以如下的构建脚本为例吧。\ndef build(bld): bld.shlib(source=\u0026#39;a.cpp b.cpp\u0026#39;, target=\u0026#39;mylib\u0026#39;) bld.program(source=\u0026#39;main.cpp\u0026#39;, target=\u0026#39;app\u0026#39;, use=\u0026#39;mylib\u0026#39;) 这里申明了一个动态库mylib,由2个文件构建而成;然后申明了一个二进制的程序app,用到了mylib。 我们用手工编译的话,需要如下的步骤:\n$ g++ -c a.cpp -o a.o $ g++ -c b.cpp -o b.o $ g++ --shared a.o b.o -o mylib.so $ g++ -c main.cpp -o main.o $ g++ main.o -o app -lmylib 其中每一行就是一个task,那么如何从build里的那几句话得到这些task呢?说来话长,要用到waf提供的一系列脚手架,就一个一个慢慢来吧!\nTask Generator解构 这两次对bld.xxx的调用,生成了2个task generator,之后task_gen经过一系列的处理,生成了5个task。 不过,task generator到底是什么?看看文档里的说法吧!\n task generator应当有如下的特征\n attribute(就是bld.shlib、bld.program的入参)仅在需要处理的时候才处理 对attribute的存在性不做要求 可以根据单个task generator来对构建过程做出调整 应该提供与插件结合的能力 所以,实现这样的一个功能还是挺难的,文档里列举了这么一些方式:\n 用类来抽象task generator,通过继承的方式来解决添加功能的问题 用python decorator来添加新功能,不过这种方式只能达到添加的功能,没办法删除已有的功能 扁平化的方式,只声明自己功能执行的时候需要满足的条件,就像面向切片编程一样。 第3个看起来不错,不如看看是怎么实现的吧。\n要产出task,那么最开始的方法是什么?是task_gen.post(),在BuildContext里被调用。这个方法其实只做了一件事情:根据设置的feature,填充task_gen.meths,处理一下里面顺序,然后挨个调用就行。\n那么就很明显了,对于所有的task generator,都有一个feature是*,而与*相互关联的方法只有2个:\n@feature(\u0026#39;*\u0026#39;) def process_source(self): ...处理bld.xyz(source=\u0026#39;a.c b.py c.tex\u0026#39;)的source @feature(\u0026#39;*\u0026#39;) @before_method(\u0026#39;process_source\u0026#39;) def process_rule(self): ...处理bld.xyz(rule=\u0026#39;cp SRC[0] TGT[0]\u0026#39;)的rule 这就是waf所有魔法的起点。有了这2个方法,好像就有了锚点一样,如果有自己的功能想要添加,就用@feature加上去,如果想要调整顺序,还有@before_method\\@after_method。\nextension mapping 在C++构建中,光有这些方法,离生成可用的task好像还有点远。其实并不远,只是需要明确到底是在哪一步hook进去的,这步就是前面提到的process_source。\n在process_source里,对source这个attribute里的每一个文件,都通过其后缀找到对应的处理函数并执行。c/c++代码的后缀无非就是c/cc/cxx/cpp/h/hpp/hxx之类的,这些waf自带的tool就已经把这些常用的都包含进去了。\n是不是有种,你以为我在第二层,其实我在第五层的感觉?实话说这就是我自己在追逻辑时候的感觉。如果feature是第一层,那么feature塞function到task_gen.meths里就是第二层;第二层提供了的一个方法process_source是第三层,process_source自己又提供了extension mapping,这就是第四层;对应到每一个extension,就可以到各自的构建过程了,这也是提供给其他的插件的hook点。\n真真儿的,有5层。\n@TaskGen.extension(\u0026#39;.cpp\u0026#39;,\u0026#39;.cc\u0026#39;,\u0026#39;.cxx\u0026#39;,\u0026#39;.C\u0026#39;,\u0026#39;.c++\u0026#39;) def cxx_hook(self, node): \u0026#34;Binds c++ file extensions to create :py:class:`waflib.Tools.cxx.cxx` instances\u0026#34; return self.create_compiled_task(\u0026#39;cxx\u0026#39;, node) 看函数名就知道了,这里就是task真正产生的地方!\n后记 其实也不能完全算看完吧,还有很多具体的细节没有提到,典型的比如变动检测(用md5而没用update time,并且联系到一个动态ID上),动态编译build function(把run_str变成一个task的方法,执行这个方法调用subprocess.Popen),以及其他种种;不过已经可以稍微帮助别人理解一下这个构建工具的基本思想,以及一个稍微具体的实例来体会构建过程,希望能起到一点抛砖引玉的作用。\n"},{"id":7,"href":"/docs/osc/ns3_typeid/","title":"TypeId in NS3","section":"Docs","content":"NS3作为一个网络仿真库,出于性能的考量选择了C++。在写仿真程序时,不可避免的要对各种实体进行建模,自然C++中的class成了唯一可选的方案。不加任何技巧的class的确可以满足对某些实体的建模,可是在仿真软件的编写中需要有足够的动态性,比如有这样一些需求:\n 动态的获知某个实体所具有的各类属性与属性的值 这个实体的状态变化后引发一系列的动作 这些都不是过分的需求,如果真的写过仿真程序的话肯定会非常渴求使用的软件能够提供实现这些需求的方法。要自己干巴巴的实现这些需求也不是不可以,比如可以提供一些查询接口来实现1;对于2的话,Qt的signal/slot或许可以实现。说到Qt了,其实QObject拥有了超越普通C++ class的能力,也都能满足上面指出的这些需求,但是其解决方案似乎有点重。\n幸好,NS3通过TypeId可以很好的解决上面提出的各类需求。\nTypeId是什么 class TypdId { uint16_t m_tid; } 这就是TypdId,就是这么简单。似乎有些不可思议,但TypdId本身只是一个16bit的整型,之前提到的所有的复杂功能都是藏在TypdId背后的IidManager完成的,这个m_tid只是作为一个索引而存在。\nTypdId提供了非常多的方法,比如说增加一个属性(AddAttribute),增加一个TraceSource(AddTraceSource),这些方法只是直接了当的将所需信息搜集起来转发给IidManager。可以看个例子:\nTypeId TypeId::AddAttribute (std::string name, std::string help, uint32_t flags, const AttributeValue \u0026amp;initialValue, Ptr\u0026lt;const AttributeAccessor\u0026gt; accessor, Ptr\u0026lt;const AttributeChecker\u0026gt; checker, SupportLevel supportLevel, const std::string \u0026amp;supportMsg) { NS_LOG_FUNCTION (this \u0026lt;\u0026lt; name \u0026lt;\u0026lt; help \u0026lt;\u0026lt; flags \u0026lt;\u0026lt; \u0026amp;initialValue \u0026lt;\u0026lt; accessor \u0026lt;\u0026lt; checker \u0026lt;\u0026lt; supportLevel \u0026lt;\u0026lt; supportMsg); IidManager::Get ()-\u0026gt;AddAttribute (m_tid, name, help, flags, initialValue.Copy (), accessor, checker, supportLevel, supportMsg); return *this; } 基本上所有的TypdId的方法都是这个样子。所以解决问题的核心其实是IidManager。IidManager可认为是一个类型数据库,保存了与TypdId想关联的Attribute与TraceSource。具体的内部实现就太细节了,作为使用方是不需要也不应该去关注的。\n从使用角度看TypdId 正如在Qt中一样,想要使自己写的一个类拥有强大的能力,需要自己动手在类的声明中添加Q_OBJECT。在NS3设计的TypeId系统中,这个步骤是要给自己的class添加一个静态方法static TypeId GetTypeId (void),然后在这个函数里返回一个TypeId。在这个过程,可以尽情的使用TypeId提供的各种方法来给本类加属性和TraceSource,唯一的限制就是这个返回的TypeId应该是处于GetTypeId里的静态变量,这是为了保证全局的唯一性。当然了,写C++的限制多了去了,这个规则应该归入这个NS3库的使用方法吧,不太值得吐槽。\nTypeId对于我们平时写程序的最大的帮助在于,它可以给自己的类添加Attribute和TraceSource。\nAttribute可以代表该实体的一些属性,比如说一台PC的IP地址、一只猫的体重等。你可能会想,这个不就是一个Get函数的事儿么,值得专门搞这么一套复杂的系统么。其实还真值得:你会去写一个127.0.0.1还是2130706433?在NS3里,可以直接写127.0.0.1,这也得归功与这个Attribute系统。\nTraceSource可类比Qt的Signal,在需要的时候调用这个Functor(想不到更好的名称了,不过写C++的应该都知道这个东西),连到这个TraceSource的其他函数(所谓的Slot)就会被自动调用。好处自不必多说,要知道Qt能得到广泛的认可,Signal/Slot功不可没,NS3里的TraceSource系统就是Signal/Slot的翻版。\n还有一个使用限制就是,需要通过一个宏来告知系统我一个TypeId要注册:NS_OBJECT_ENSURE_REGISTERED。这个宏其实声明了一个静态类并同时声明了一个本文件内的静态实例。在这个静态类的构造函数中调用了我们定义的类的GetTypeId,这就实现了自定义TypeId的注册。\nAttribute与TraceSource 终于到重头戏了。其实这两部分的代码都切实的体现了C++罪恶的地方:模板与宏。一个新手要看懂这些代码要补充太多的东西了。说来惭愧,其实我自身也是一个新手,从开始接触这个库到现在能初步搞明白这样一个系统(真的只是大致初步明白),已经过去了3年多。这个系统的实现是模板里套宏、宏里套模板,看的时候需要时刻注意这段代码是宏生成的代码还是用了模板。\nAttribute 前面提到了我们可以用127.0.0.1来代表ip的那32个bit,这就是Attribute系统的神奇所在。在这个例子里,127.0.0.1其实是一个字符串,Attribute系统可以把字符串转化为任何一种类型(可以是自定义类型)。\n就单纯的以这个地址类型来解释好了。我们的程序中需要使用IP地址,其最合适的存储方式其实是一个int,但IP地址最适合人类的表述方式其实是点分表示,我们自然也想在使用的时候用这种方式。那这个应该怎么做?\n首先先不管人怎么读写的问题,先考虑程序中的这个属性的使用方式。作为一个Ipv4的值,肯定有一些相关联的方法,比如说是否为广播地址、是否为多播地址、是否为本地地址之类类的。这些可以以用成员函数的方式实现,既然这样,那就尽情的实现吧!不需要考虑怎么集成到Attribute系统中去。同理,这个类里面有什么字段,想要什么字段就尽情的加。想必你也看出来了,我们在实现一个Attribute的时候,其实根本不需要考虑什么集成的问题。\n能够用ns3的方式来给一个对象设置属性的这个能力依赖与3个基本的组件\n AttributeValue AttributeAccessor AttributeChecker 首先看看什么是ns3的方式为一个对象设置属性,看一下官方manual里的例子\nPtr\u0026lt;ConfigExample\u0026gt; a2_obj = CreateObject\u0026lt;ConfigExample\u0026gt; (); a2_obj-\u0026gt;SetAttribute (\u0026#34;TestInt16\u0026#34;, IntegerValue (-3)); IntegerValue iv; a2_obj-\u0026gt;GetAttribute (\u0026#34;TestInt16\u0026#34;, iv); 第一行创建了新的对象ConfigExample,并存在指针a2_obj里。第二行就是所谓的ns3的方式设置属性,依赖于一个方法SetAttriute。这个方法属于ObjectBase,所有能用Ptr指向的对象都是objectBase的子类。所以说,在调用SetAttribute时,除去C++的语法糖,这句话完整的形式是这样的:\nSetAttribute (a2_obj, \u0026#34;TestInt16\u0026#34;, IntegerValue (-3)); 好了,我们跳进去看看实现\nvoid ObjectBase::SetAttribute (std::string name, const AttributeValue \u0026amp;value) { NS_LOG_FUNCTION (this \u0026lt;\u0026lt; name \u0026lt;\u0026lt; \u0026amp;value); struct TypeId::AttributeInformation info; TypeId tid = GetInstanceTypeId (); if (!tid.LookupAttributeByName (name, \u0026amp;info)) { NS_FATAL_ERROR (\u0026#34;Attribute name=\u0026#34;\u0026lt;\u0026lt;name\u0026lt;\u0026lt;\u0026#34; does not exist for this object: tid=\u0026#34;\u0026lt;\u0026lt;tid.GetName ()); } if (!(info.flags \u0026amp; TypeId::ATTR_SET) || !info.accessor-\u0026gt;HasSetter ()) { NS_FATAL_ERROR (\u0026#34;Attribute name=\u0026#34;\u0026lt;\u0026lt;name\u0026lt;\u0026lt;\u0026#34; is not settable for this object: tid=\u0026#34;\u0026lt;\u0026lt;tid.GetName ()); } if (!DoSet (info.accessor, info.checker, value)) { NS_FATAL_ERROR (\u0026#34;Attribute name=\u0026#34;\u0026lt;\u0026lt;name\u0026lt;\u0026lt;\u0026#34; could not be set for this object: tid=\u0026#34;\u0026lt;\u0026lt;tid.GetName ()); } } 这个方法是对象自己身上的方法,所以要记住this这时候指向的是谁:这里就是a2_obj。这个方法也很直白\n 首先能通过GetInstanceTypdId()拿到真正的、ConfigExample的TypeId 拿到AttributeInformation,就有了accessor、checker,还有作为参数传进来的值value。 DoSet做了实际的设置工作 再看看DoSet:\nbool ObjectBase::DoSet (Ptr\u0026lt;const AttributeAccessor\u0026gt; accessor, Ptr\u0026lt;const AttributeChecker\u0026gt; checker, const AttributeValue \u0026amp;value) { NS_LOG_FUNCTION (this \u0026lt;\u0026lt; accessor \u0026lt;\u0026lt; checker \u0026lt;\u0026lt; \u0026amp;value); Ptr\u0026lt;AttributeValue\u0026gt; v = checker-\u0026gt;CreateValidValue (value); if (v == 0) { return false; } bool ok = accessor-\u0026gt;Set (this, *v); return ok; } 检查什么的就不说了,最让人关心的是这个方法accessor-\u0026gt;Set (this, *v)。这个方法是怎么定义的,是哪里来的?这下欢迎进入模板与宏的世界。\nAttributeAccessor 答案是这个这个方法是属于accessor的,而accessor的定义是在注册TypeId的时候生成的。RTFSC:\nclass ConfigExample : public Object { public: static TypeId GetTypeId (void) { static TypeId tid = TypeId (\u0026#34;ns3::A\u0026#34;) .SetParent\u0026lt;Object\u0026gt; () .AddAttribute (\u0026#34;TestInt16\u0026#34;, \u0026#34;help text\u0026#34;, IntegerValue (-2), MakeIntegerAccessor (\u0026amp;A::m_int16), MakeIntegerChecker\u0026lt;int16_t\u0026gt; ()) ; return tid; } int16_t m_int16; }; NS_OBJECT_ENSURE_REGISTERED (ConfigExample); 看到那句MakeIntegerAccessor (\u0026amp;A::m_int16)了么?搞懂了这个,其实就能搞懂ns3的套路了,再看其他的机制也就顺风顺水了。我们慢慢来,一步一步来,保证每一步都有始有终,不会出现跳跃的现象。这个过程稍微有点冗长,可以去拿包零食边吃边看了。\nMakeIntegerAccessor是可调用的一个“东西”。回想一下C++可调用的东西有哪些?1. 函数,2. Functor,就是实现了自定义operator()的一个class的实例。3.实例化一个类型看起来也像是函数调用。我用的Eclipse,f3跳转到定义,等我过去的时候傻眼了:\nATTRIBUTE_ACCESSOR_DEFINE (Integer); 好家伙,看来要展开这个看看了,ctrl+=让它现形:\ntemplate \u0026lt;typename T1\u0026gt; \\ Ptr\u0026lt;const AttributeAccessor\u0026gt; MakeIntegerAccessor (T1 a1) \\ { \\ return MakeAccessorHelper\u0026lt;IntegerValue\u0026gt; (a1); \\ } \\ template \u0026lt;typename T1, typename T2\u0026gt; \\ Ptr\u0026lt;const AttributeAccessor\u0026gt; MakeIntegerAccessor (T1 a1, T2 a2) \\ { \\ return MakeAccessorHelper\u0026lt;IntegerValue\u0026gt; (a1, a2); \\ } 这展开了2个函数,到这时可以确定,MakeIntegerAccessor是一个函数,而且我们调用的是只有一个入参的那个函数,这个函数返回了一个AttributeAccessor的智能指针。具体的展开过程就不细讲了,也没有讲的必要,看看ATTRIBUTE_ACCESSOR_DEFINE的定义就明白了。现在需要关心的是我们现在调用的函数里有个T1,要搞明白这个T1的类型是什么。\n重新回头看看MakeIntegerAccessor (\u0026amp;A::m_int16),这里的T1就是\u0026amp;A::m_int16的类型。先就此打住,这个结论先记下来。我们继续追下去,这下应该看真正的实现MakeAccessorHelper\u0026lt;IntegerValue\u0026gt; (a1):\n// 第一种实现 template \u0026lt;typename V, typename T, typename U\u0026gt; inline Ptr\u0026lt;const AttributeAccessor\u0026gt; DoMakeAccessorHelperOne (U T::*memberVariable) // 第二种实现 template \u0026lt;typename V, typename T, typename U\u0026gt; inline Ptr\u0026lt;const AttributeAccessor\u0026gt; DoMakeAccessorHelperOne (U (T::*getter)(void) const) // 第三种实现 template \u0026lt;typename V, typename T, typename U\u0026gt; inline Ptr\u0026lt;const AttributeAccessor\u0026gt; DoMakeAccessorHelperOne (void (T::*setter)(U)) 结果就是匹配到了第一种实现。 其实我曾经很多次追到了这里,却没看懂这里的类型到底是什么意思。也不知道什么时候忽然就明白了。A::m_int16对应于U T::*,是个正常人第一眼看上去绝对不会明白这到底是怎么联系在一起的,我也是正常人,所以我现在也不明白这种怪异的语法到底是谁第一次使用的。T对应于A,那么U应该是对应于m_int16。这个类型能代表一个类里的一个成员变量的类型,T表明了它是一个类的成员变量,U表明了这个变量的类型是uint16_t,现在就只能这么死记了,要想真正搞明白我觉得应该去翻一下编译器里前端到底是怎么解析这个鬼畜般的语法的,先就这么囫囵吞枣吧!对于另外的两个反而更好懂一点,那个类型和平时用的函数指针类型声明挺像的,反而不用多说。一个是getter,说明这个attribute只提供了获取的接口;一个是setter,说明这个attribute只能设置不能获取。当然了,这是站在ns3的使用方式上说的,直接强行用c++的方式赋值不在我们的讨论范围之内。\n这3个函数都返回了一个指向AttributeAccessor的指针。现在来看看实现吧!\nPtr\u0026lt;const AttributeAccessor\u0026gt; DoMakeAccessorHelperOne (U T::*memberVariable) { /* AttributeAcessor implementation for a class member variable. */ class MemberVariable : public AccessorHelper\u0026lt;T,V\u0026gt; { public: /* * Construct from a class data member address. * \\param [in] memberVariable The class data member address. */ MemberVariable (U T::*memberVariable) : AccessorHelper\u0026lt;T,V\u0026gt; (), m_memberVariable (memberVariable) {} private: virtual bool DoSet (T *object, const V *v) const { typename AccessorTrait\u0026lt;U\u0026gt;::Result tmp; bool ok = v-\u0026gt;GetAccessor (tmp); if (!ok) { return false; } (object-\u0026gt;*m_memberVariable) = tmp; return true; } virtual bool DoGet (const T *object, V *v) const { v-\u0026gt;Set (object-\u0026gt;*m_memberVariable); return true; } virtual bool HasGetter (void) const { return true; } virtual bool HasSetter (void) const { return true; } U T::*m_memberVariable; // Address of the class data member. }; return Ptr\u0026lt;const AttributeAccessor\u0026gt; (new MemberVariable (memberVariable), false); } 照样很鬼畜。这是在函数里定义了一个类,并且返回了指向这个类的只能指针。这个类继承自AccessorHelper,而AccessorHelper又继承自AttributeAccessor。所以将其作为AttributeAccessor的子类返回也说得过去。\n至于为什么要继承这么多?我现在的理解是这样的\n AttributeAccessor只是一个纯虚接口,它只定义了作为Accessor应当具有的接口。在Java里的话,估计这就是个Interface。 AccessorHelper提供了Set和Get的默认实现,把一些可变的部分留给了它的子类来实现,这些可变的部分是DoSet和DoGet。所以在MemberVariable要实现DoSet和DoGet。这应该是某种设计模式,看看那本书就能找到了。 到现在为止,我们知道可以造出来一个AttributeAccessor,并把指向这个AttributeAccessor的指针存在了我们的IidManager的数据库中。以后想要拿出来这个AttributeAccessor,就要手拿TypeId去找IidManager去要,而且要到的也是一个指针,这个指针指向了在return Ptr\u0026lt;const AttributeAccessor\u0026gt; (new MemberVariable (memberVariable), false);这句话里的那个new出来的地址。\n总结一下,一个类型的AttributeAccessor只有一个,就是那个new出来的地方。程序其他地方都是拿指针去访问的。在那块地址存的东西只有两样(只考虑我们现在这个membervariable类型的accessor)\n U T::*m_memberVariable的值,这个值代表了这个变量在拥有TypeId那个类里的偏移量 一个虚表指针。因为是有virtual函数,所以这个AttributeAccessor的实例是有虚表指针的,这个虚表里就是真正的、对应类型的函数实现。 回头看看那个DoSet,里面那个accessor到底是什么应该已经清楚了。那个个accessor的Set方法在哪儿定义的?答案是AccessorHelper。我直接把结论公开了,但是你现在应该停下来去看看具体的实现。AttributeAccessor-\u0026gt;AccessorHelper-\u0026gt;DoMakeAccessorHelperOne()里的MemberVariable这是一条继承链,到了最下一层的时候所有的方法都已经定义,只是在不同的层次提供了不同的实现。\n假设你已经搞明白Accessor的继承链条了,也明白这个Accessor到底支持什么操作,我们就进入了真正执行Set的地方:\n// DoMakeAccessorHelperOne()里的MemberVariable的方法 virtual bool DoSet (T *object, const V *v) const { typename AccessorTrait\u0026lt;U\u0026gt;::Result tmp; bool ok = v-\u0026gt;GetAccessor (tmp); if (!ok) { return false; } (object-\u0026gt;*m_memberVariable) = tmp; return true; } 这里的V *v就是myObj-\u0026gt;SetAttribute (\u0026quot;MyIntAttribute\u0026quot;, IntegerValue(3));里的IntegerValue(3)。要是没看懂,就去翻代码。这个结论是必须要搞懂的,不然就没有进行下去的必要了。\n其实Accessor的世界已经探索的差不多了,为了真正搞明白这个函数做了什么,我们先转向看看AttributeValue。\nAttributeValue NS3的套路是什么?用宏和模板做代码生成。这个套路在AttributeValue里也是一样的。自定义了一个AttributeValue需要写一个宏,这个宏帮助我们做了大部分的事情。拿那个IntegerValue说事儿:\nATTRIBUTE_VALUE_DEFINE_WITH_NAME (uint64_t, Uinteger); // 在头文件里写这个宏,能够展开为如下的定义 class UintegerValue : public AttributeValue \\ { \\ public: \\ UintegerValue (); \\ UintegerValue (const uint64_t \u0026amp;value); \\ void Set (const uint64_t \u0026amp;value); \\ uint64_t Get (void) const; \\ template \u0026lt;typename T\u0026gt; \\ bool GetAccessor (T \u0026amp;value) const { \\ value = T (m_value); \\ return true; \\ } \\ virtual Ptr\u0026lt;AttributeValue\u0026gt; Copy (void) const; \\ virtual std::string \\ SerializeToString (Ptr\u0026lt;const AttributeChecker\u0026gt; checker) const; \\ virtual bool \\ DeserializeFromString (std::string value, \\ Ptr\u0026lt;const AttributeChecker\u0026gt; checker); \\ private: \\ uint64_t m_value; \\ } // 上述定义的实现仰赖于对于cc文件里的实现,也是用宏 ATTRIBUTE_VALUE_IMPLEMENT_WITH_NAME (uint64_t,Uinteger); // 展开后是这样的 UintegerValue::UintegerValue () \\ : m_value () {} \\ UintegerValue::UintegerValue (const uint64_t \u0026amp;value) \\ : m_value (value) {} \\ void UintegerValue::Set (const uint64_t \u0026amp;v) { \\ m_value = v; \\ } \\ uint64_t UintegerValue::Get (void) const { \\ return m_value; \\ } \\ Ptr\u0026lt;AttributeValue\u0026gt; \\ UintegerValue::Copy (void) const { \\ return ns3::Create\u0026lt;UintegerValue\u0026gt; (*this); \\ } \\ std::string UintegerValue::SerializeToString \\ (Ptr\u0026lt;const AttributeChecker\u0026gt; checker) const { \\ std::ostringstream oss; \\ oss \u0026lt;\u0026lt; m_value; \\ return oss.str (); \\ } \\ bool UintegerValue::DeserializeFromString \\ (std::string value, Ptr\u0026lt;const AttributeChecker\u0026gt; checker) { \\ std::istringstream iss; \\ iss.str (value); \\ iss \u0026gt;\u0026gt; m_value; \\ do { \\ if (!(iss.eof ())) \\ { \\ std::cerr \u0026lt;\u0026lt; \u0026#34;aborted. cond=\\\u0026#34;\u0026#34; \u0026lt;\u0026lt; \u0026#34;!(iss.eof ())\u0026#34; \u0026lt;\u0026lt; \u0026#34;\\\u0026#34;, \u0026#34;; \\ do \\ { \\ std::cerr \u0026lt;\u0026lt; \u0026#34;msg=\\\u0026#34;\u0026#34; \u0026lt;\u0026lt; \u0026#34;Attribute value \u0026#34; \u0026lt;\u0026lt; \u0026#34;\\\u0026#34;\u0026#34; \u0026lt;\u0026lt; value \u0026lt;\u0026lt; \u0026#34;\\\u0026#34;\u0026#34; \\ \u0026lt;\u0026lt; \u0026#34; is not properly formatted\u0026#34; \u0026lt;\u0026lt; \u0026#34;\\\u0026#34;, \u0026#34;; \\ do \\ { \\ std::cerr \u0026lt;\u0026lt; \u0026#34;file=\u0026#34; \u0026lt;\u0026lt; \u0026#34;D:\\\\Code\\\\ns-allinone-3.28\\\\ns-3.28\\\\src\\\\core\\\\model\\\\uinteger.cc\u0026#34; \u0026lt;\u0026lt; \u0026#34;, line=\u0026#34; \u0026lt;\u0026lt; \\ 35 \u0026lt;\u0026lt; std::endl; \\ ::ns3::FatalImpl::FlushStreams (); \\ if (true) std::terminate (); \\ } \\ while (false); \\ } \\ while (false); \\ } \\ } while (false); \\ return !iss.bad () \u0026amp;\u0026amp; !iss.fail (); \\ } 具体的展开过程感兴趣的可以去追一下,要是没有IDE的帮助,要展开一个这么复杂的宏也还是需要一点时间的。结合之前accessor的内容(v-\u0026gt;GetAccessor),这个函数就定义在了这里(头文件里作为模板类成员函数实现了)。\n值得一看的倒是那两个SerializeToString和DeserializerFromString。这两个函数完成了字符串到实际定义类型的转换,里面用到了\u0026lt;\u0026lt;的重载,所以这也是为什么在自定义属性的时候要去实现全局operator\u0026lt;\u0026lt;的原因了。通过这两个函数,我们就可以用一个字面意义上的127.0.0.1去设置一个IP,而非2130706433。其实这个系统在解析string的时候出了一点小bug被我发现了,也算是对开源的一点点小贡献吧!(https://www.nsnam.org/bugzilla/show_bug.cgi?id=2447) 这个bug在ns3.25里提出来,之后应该是修好了。\n还剩一个AttributeChecker,但这个好像不影响对系统的理解,就不去看啦!想必搞明白套路之后,要看懂也不是什么难事儿啦!\nTraceSource Callback杂谈 说起TraceSource,那么Callback就是绕不过去的坎。可以作为一个TraceSource的属性关联一个TracedCallback类型,用于通知自身值的改变。TracedCallback只是一个Callback的容器,里面有一个std::list\u0026lt;Callback\u0026gt;用于存放连接到该TraceSource的函数。一个TraceSource可以连接多次,每次它被Connect一次,就会往这个list里填一个元素。当然,这个元素就是一个Callback。\nCallback类本身只是提供了创建的接口于调用接口。调用接口就是对各个operator()的重载,最多有9个参数,也因此有9个operator()。这种情况在c++11之后应该会有更好的写法,只是我并不知道怎么写罢了。Callback继承自CallbackBase,这里存放了真正的指向实现(CallbackImplBase)的指针。\n怎么CallbaciImpl还有继承?从Callback开始已经跳转两次了还没见到真正的实现,其实这也不远了,CallbackImplBase说到底就是一个Interface一样的东西,对CallbackImpl做了一些限定,这样继承了CallbackImpl的子类就能以比较一致的方法去操作。其实CallbackImplBase-\u0026gt;CallbackImpl-\u0026gt;各种具体的CallbackImpl弄这么复杂也是无奈之举。抽出来中间的CallbackImpl是为了实现多输入参数类型的operator()的重载,考虑到这个库的编写时间,那时候的c++模板编程好像没有c++11之后的那么完善,没有可变长类型参数,这样做也是无可厚非。我想如果用最新的c++11之后的标准来写,这个Callback可能就不会这么难以理解了,似乎可以直接采用std::function或者只是做一些小的改动就可以了。不管怎样,到了CallbackImpl这个层级时,单单CallbackImpl这个名字就已经可以支撑多达9个入参的operator()了,再下面层级的类就可以方便的享用这种便利,这也是为什么MemPtrCallbackImpl、FunctorCallbackImpl等子类可以在一个class里就重载多次operator()同时还能做类型检查的原因了。\nTracedValue追踪 先来看看在TypeId里怎么使用TraceSource吧!我随便从代码里摘了一条出来\n.AddTraceSource (\u0026#34;Tx\u0026#34;, \u0026#34;A new packet is created and is sent\u0026#34;, MakeTraceSourceAccessor (\u0026amp;BulkSendApplication::m_txTrace), \u0026#34;ns3::Packet::TracedCallback\u0026#34;) 又看到了熟悉的Accessor,这个Accessor为的就是能拿到类里的一个成员变量。所幸对于TraceSource来说,只存在访问Get而不存在设置Set,这个Accessor相比起AttributeAccessor来说要简单一些。追到代码里看到的还是熟悉的的套路:\ntemplate \u0026lt;typename T\u0026gt; Ptr\u0026lt;const TraceSourceAccessor\u0026gt; MakeTraceSourceAccessor (T a) { return DoMakeTraceSourceAccessor (a); } // DoMakeTraceSourceAccessor的实现 // 在函数内部定义新的类,这个类实现了TraceSourceAccessor的接口 // 因为`TraceSource`只有一种类型,这种类型就是 // “类内部的成员变量” // 所以可以看到函数的签名就只有一种 // SOURCE T::a // sigh...又是这个鬼畜的标记 template \u0026lt;typename T, typename SOURCE\u0026gt; Ptr\u0026lt;const TraceSourceAccessor\u0026gt; DoMakeTraceSourceAccessor (SOURCE T::*a) { struct Accessor : public TraceSourceAccessor { virtual bool ConnectWithoutContext (ObjectBase *obj, const CallbackBase \u0026amp;cb) const { T *p = dynamic_cast\u0026lt;T*\u0026gt; (obj); if (p == 0) { return false; } (p-\u0026gt;*m_source).ConnectWithoutContext (cb); return true; } virtual bool Connect (ObjectBase *obj, std::string context, const CallbackBase \u0026amp;cb) const { T *p = dynamic_cast\u0026lt;T*\u0026gt; (obj); if (p == 0) { return false; } (p-\u0026gt;*m_source).Connect (cb, context); return true; } virtual bool DisconnectWithoutContext (ObjectBase *obj, const CallbackBase \u0026amp;cb) const { T *p = dynamic_cast\u0026lt;T*\u0026gt; (obj); if (p == 0) { return false; } (p-\u0026gt;*m_source).DisconnectWithoutContext (cb); return true; } virtual bool Disconnect (ObjectBase *obj, std::string context, const CallbackBase \u0026amp;cb) const { T *p = dynamic_cast\u0026lt;T*\u0026gt; (obj); if (p == 0) { return false; } (p-\u0026gt;*m_source).Disconnect (cb, context); return true; } SOURCE T::*m_source; } *accessor = new Accessor (); accessor-\u0026gt;m_source = a; return Ptr\u0026lt;const TraceSourceAccessor\u0026gt; (accessor, false); } TraceSource存在的理由就是要触发其他的逻辑的,因此要提供挂接其他逻辑的方法,即Connect。这里的Accessor只是简单的把Connect的请求转发给了TraceSource。好几个类都有Connect,一不小心就晕头转向了,现在可以总结一下不同类的Connect到底做了什么,以及它们究竟时何时被调用的。\n假设现在已经有一个ns3的类MyObject(fifth.cc),也继承了Object,意味着它实现了GetTypeId,拥有了Attribute和TraceSource的能力。它有一个可以被trace的值m_myInt。这个值不是简单的类型,而是一个TracedValue\u0026lt;int32_t\u0026gt; m_myInt;。这样的话,只要对m_myInt进行赋值,Trace系统就可以工作了。给这个m_myInt赋值的话会调用什么?当然是operator=了。跳到TracedValue的对应实现看看:\nTracedValue \u0026amp;operator = (const TracedValue \u0026amp;o) { TRACED_VALUE_DEBUG (\u0026#34;x=\u0026#34;); Set (o.m_v); return *this; } // 关键就在`Set`里了,里面肯定有触发`Callback`的代码 void Set (const T \u0026amp;v) { if (m_v != v) { m_cb (m_v, v); m_v = v; } } 果然,有个m_cb,这就是我们之前提到的TracedCallback。每次给这个m_myInt赋值,就会调用m_cb通知这个值已经变化。可以看到,TracedValue本身提供了一个Connect的方法,这意味着我们可以直接用m_myInt-\u0026gt;Connect来把自己的处理函数连接上去。但是实际中往往是通过myObj-\u0026gt;ConnectWithoutContext(\u0026quot;myInt\u0026quot;, MakeCallback(\u0026amp;mycallback))这样的方式。\nbool ObjectBase::TraceConnectWithoutContext (std::string name, const CallbackBase \u0026amp;cb) { NS_LOG_FUNCTION (this \u0026lt;\u0026lt; name \u0026lt;\u0026lt; \u0026amp;cb); TypeId tid = GetInstanceTypeId (); Ptr\u0026lt;const TraceSourceAccessor\u0026gt; accessor = tid.LookupTraceSourceByName (name); if (accessor == 0) { return false; } bool ok = accessor-\u0026gt;ConnectWithoutContext (this, cb); return ok; } Accessor我们之前已经讲过了,是一个在GetTypeId里被调用并生成的一个类型,具体的accessor-\u0026gt;ConnectWithoutContext在上面的DoMakeTraceSourceAccessor里有定义,还是通过了SOURCE T::*a这个类型得到了TracedValue在类中的位置,调用了这个类型的ConnectWithoutContext。\n至于TracedCallback,理解起来就没什么难度了,在这个类型的operator()里,注册进来的Callback以此执行一遍即可。\n所以在运行是整个trace的过程是这样的:\n m_myInt=1;会跑到TracedValue的operator= TracedValue::operator=调用了与m_myInt相关联的TracedCallback::operator() TracedCallback::operator()以此调用事先注册好的Callback。 总结 IidManager是一个数据库,TypeId是数据库的key,Attribute和TraceSource是数据库的两张表。每在GetTypeId里AddAttribute或者AddTraceSouce一次就相当于给表里加一行记录,所有与Attribute、TraceSource相关的操作都会去表里找自己需要的信息。 大量运用了模板和宏来生成一些框架代码,比如Accessor。这也是代码难以理解之处的根本所在。熟悉一些模板的套路,比如CRTP(在object模型里用到)、PIMPL(callback里用到);熟悉一些c++里的编程套路,比如在函数内部定义class(Accessor里用到),静态变量初始化(保证自定义Object可被注册时用到);以及字面意义上的代码生成(宏,#与##),类型这个层次的代码生成(template,多个类型参数,traits),都是需要去细心体会的。 后记 C++真难。\n这套代码看下来,也是让人惊叹C++真的是没有做不到,只有想不到。那些模板和宏生成代码的套路,基本上把能在编译期算的都在编译器搞定,能在初始化搞的全在初始化时完成,运行时跑得都是很难再简化的必要的逻辑。其实网络仿真程序也基本算一个\u0026quot;well-defined\u0026quot;的东西,有着明确的需求,又是开源项目,可以花心思把系统设计的如此巧妙。\n希望自己以后能有机会能从头参与类似项目的开发,而不是在反复无常的业务逻辑上消磨时光。\n"}]