#633 继续:全能程序员 vs 特长程序员
阮一峰 2021-09-10上次讨论了阮一峰关于全能程序员与特长程序员的观点,末尾说期待他继续聊全能程序员的优势,然后发现今天发布的新一期的《科技爱好者周刊》果然在说这个。
coding in a complicated world
上次讨论了阮一峰关于全能程序员与特长程序员的观点,末尾说期待他继续聊全能程序员的优势,然后发现今天发布的新一期的《科技爱好者周刊》果然在说这个。
我定义了一个 gitg 的快捷方式:
function _new_gitg () {
# alias g='gitg --all >/dev/null 2>&1 &' # 覆盖 g=git
if [ -z "$1" ]; then
nohup gitg --all >/dev/null 2>&1 &!
else
nohup gitg -s $1 >/dev/null 2>&1 &!
fi
}
alias g='_new_gitg'
但是每次退出 shell 会有提示:zsh: you have running jobs.
我就加上 nohup,以为不会有提示,但提示依旧(不过强制退出也不会关闭 gitg 就是了)。
在 SO 上搜到相同问题,这个答复就可以很好的解决我的问题:
If you want to not see that message, simply pass the job id to disown, like so:
> disown %1 > ``` > > Or, start the job with &! (zsh-specific trick): > > ```sh > nohup ./my_script.sh &! > ``` ```sh function _new_gitg () { if [ -z "$1" ]; then nohup gitg --all >/dev/null 2>&1 &! else nohup gitg -s $1 >/dev/null 2>&1 &! fi } alias g='_new_gitg'
经过测试,这个技巧在 bash 5.1.4 下也有效。
在网络包的发送和接收过程中,绝大部分的工作都是在内核态完成的。那么问题来了,我们常用的运行在用户态的程序 tcpdump 是那如何实现抓到内核态的包的呢?有的同学知道 tcpdump 是基于 libpcap 的,那么 libpcap 的工作原理又是啥样的呢。如果让你裸写一个抓包程序,你有没有思路?
按照飞哥的风格,不搞到最底层的原理咱是不会罢休的。所以我对相关的源码进行了深入分析。通过本文,你将彻底搞清楚了以下这几个问题。
借助这几个问题,我们来展开今天的探索之旅!
在图解 Linux 网络包接收过程一文中我们详细介绍了网络包是如何从网卡到达用户进程中的。这个过程我们可以简单用如下这个图来表示。
我们在网络设备层的代码里找到了 tcpdump 的抓包入口。在 __netif_receive_skb_core 这个函数里会遍历 ptype_all 上的协议。还记得上文中我们提到 tcpdump 在 ptype_all 上注册了虚拟协议。这时就能执行的到了。来看函数:
//file: net/core/dev.c
static int __netif_receive_skb_core(struct sk_buff *skb, bool pfmemalloc)
{
......
//遍历 ptype_all (tcpdump 在这里挂了虚拟协议)
list_for_each_entry_rcu(ptype, &ptype_all, list) {
if (!ptype->dev || ptype->dev == skb->dev) {
if (pt_prev)
ret = deliver_skb(skb, pt_prev, orig_dev);
pt_prev = ptype;
}
}
}
在上面函数中遍历 ptype_all,并使用 deliver_skb 来调用协议中的回调函数。
//file: net/core/dev.c
static inline int deliver_skb(...)
{
return pt_prev->func(skb, skb->dev, pt_prev, orig_dev);
}
对于 tcpdump 来说,就会进入 packet_rcv 了(后面我们再说为啥是进入这个函数)。这个函数在 net/packet/af_packet.c 文件中。
//file: net/packet/af_packet.c
static int packet_rcv(struct sk_buff *skb, ...)
{
__skb_queue_tail(&sk->sk_receive_queue, skb);
......
}
可见 packet_rcv 把收到的 skb 放到了当前 packet socket 的接收队列里了。这样后面调用 recvfrom 的时候就可以获取到所抓到的包!!
为了解释我们开篇中提到的问题,这里我们再稍微到协议层中多看一些。在 ip_rcv 中我们找到了一个 netfilter 相关的执行逻辑。
//file: net/ipv4/ip_input.c
int ip_rcv(...)
{
......
return NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING, skb, dev, NULL,
ip_rcv_finish);
}
如果你用 NF_HOOK 作为关键词来搜索,还能搜到不少 netfilter 的过滤点。不过所有的过滤点都是位于 IP 协议层的。
在接收包的过程中,数据包是先经过网络设备层然后才到协议层的。
那么我们开篇中的一个问题就有了答案了。假如我们设置了 netfilter 规则,在接收包的过程中,工作在网络设备层的 tcpdump 先开始工作。还没等 netfilter 过滤,tcpdump 就抓到包了!
所以,在接收包的过程中,netfilter 过滤并不会影响 tcpdump 的抓包!
我们接着再来看网络包发送过程。在25 张图,一万字,拆解 Linux 网络包发送过程一文中,我们详细描述过网络包的发送过程。发送过程可以汇总成简单的一张图。
在发送的过程中,同样是在 IP 层进入各种 netfilter 规则的过滤。
//file: net/ipv4/ip_output.c
int ip_local_out(struct sk_buff *skb)
{
//执行 netfilter 过滤
err = __ip_local_out(skb);
}
int __ip_local_out(struct sk_buff *skb)
{
......
return nf_hook(NFPROTO_IPV4, NF_INET_LOCAL_OUT, skb, NULL,
skb_dst(skb)->dev, dst_output);
}
在这个文件中,还能看到若干处 netfilter 过滤逻辑。
发送过程在协议层处理完毕到达网络设备层的时候,也有 tcpdump 的抓包点。
//file: net/core/dev.c
int dev_hard_start_xmit(struct sk_buff *skb, struct net_device *dev, struct netdev_queue *txq)
{
...
if (!list_empty(&ptype_all))
dev_queue_xmit_nit(skb, dev);
}
static void dev_queue_xmit_nit(struct sk_buff *skb, struct net_device *dev)
{
list_for_each_entry_rcu(ptype, &ptype_all, list) {
if ((ptype->dev == dev || !ptype->dev) && (!skb_loop_sk(ptype, skb))) {
if (pt_prev) {
deliver_skb(skb2, pt_prev, skb->dev);
pt_prev = ptype;
continue;
}
......
}
}
}
在上述代码中我们看到,在 dev_queue_xmit_nit 中遍历 ptype_all 中的协议,并依次调用 deliver_skb。这就会执行到 tcpdump 挂在上面的虚拟协议。
在网络包的发送过程中,和接收过程恰好相反,是协议层先处理、网络设备层后处理。
如果 netfilter 设置了过滤规则,那么在协议层就直接过滤掉了。在下层网络设备层工作的 tcpdump 将无法再捕获到该网络包。
前面两小节我们说到了内核收发包都通过遍历 ptype_all 来执行抓包的。那么我们现在来看看用户态的 tcpdump 是如何挂载协议到内 ptype_all 上的。
我们通过 strace 命令我们抓一下 tcpdump 命令的系统调用,显示结果中有一行 socket 系统调用。Tcpdump 秘密的源头就藏在这行对 socket 函数的调用里。
# strace tcpdump -i eth0
socket(AF_PACKET, SOCK_RAW, 768)
......
socket 系统调用的第一个参数表示创建的 socket 所属的地址簇或者协议簇,取值以 AF 或者 PF 开头。在 Linux 里,支持很多种协议族,在 include/linux/socket.h 中可以找到所有的定义。这里创建的是 packet 类型的 socket。
协议族和地址族:每一种协议族都有其对应的地址族。比如 IPV4 的协议族定义叫 PF_INET,其地址族的定义是 AF_INET。它们是一一对应的,而且值也完全一样,所以经常混用。
//file: include/linux/socket.h
#define AF_UNSPEC 0
#define AF_UNIX 1 /* Unix domain sockets */
#define AF_LOCAL 1 /* POSIX name for AF_UNIX */
#define AF_INET 2 /* Internet IP Protocol */
#define AF_INET6 10 /* IP version 6 */
#define AF_PACKET 17 /* Packet family */
......
另外上面第三个参数 768 代表的是 ETH_P_ALL,socket.htons(ETH_P_ALL) = 768。
我们来展开看这个 packet 类型的 socket 创建的过程中都干了啥,找到 socket 创建源码。
//file: net/socket.c
SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol)
{
......
retval = sock_create(family, type, protocol, &sock);
}
int __sock_create(struct net *net, int family, int type, ...)
{
......
pf = rcu_dereference(net_families[family]);
err = pf->create(net, sock, protocol, kern);
}
在 __sock_create 中,从 net_families 中获取了指定协议。并调用了它的 create 方法来完成创建。
net_families 是一个数组,除了我们常用的 PF_INET( ipv4 ) 外,还支持很多种协议族。比如 PF_UNIX、PF_INET6(ipv6)、PF_PACKET 等等。每一种协议族在 net_families 数组的特定位置都可以找到其 family 类型。在这个 family 类型里,成员函数 create 指向该协议族的对应创建函数。
根据上图,我们看到对于 packet 类型的 socket,pf->create 实际调用到的是 packet_create 函数。我们进入到这个函数中来一探究竟,这是理解 tcpdump 工作原理的关键!
//file: packet/af_packet.c
static int packet_create(struct net *net, struct socket *sock, int protocol, int kern)
{
...
po = pkt_sk(sk);
po->prot_hook.func = packet_rcv;
//注册钩子
if (proto) {
po->prot_hook.type = proto;
register_prot_hook(sk);
}
}
static void register_prot_hook(struct sock *sk)
{
struct packet_sock *po = pkt_sk(sk);
dev_add_pack(&po->prot_hook);
}
在 packet_create 中设置回调函数为 packet_rcv,再通过 register_prot_hook => dev_add_pack 完成注册。注册完后,是在全局协议 ptype_all 链表中添加了一个虚拟的协议进来。
我们再来看下 dev_add_pack 是如何注册协议到 ptype_all 中的。回顾我们开头看到的 socket 函数调用,第三个参数 proto 传入的是 ETH_P_ALL。那 dev_add_pack 其实最后是把 hook 函数添加到了 ptype_all 里了,代码如下。
//file: net/core/dev.c
void dev_add_pack(struct packet_type *pt)
{
struct list_head *head = ptype_head(pt);
list_add_rcu(&pt->list, head);
}
static inline struct list_head *ptype_head(const struct packet_type *pt)
{
if (pt->type == htons(ETH_P_ALL))
return &ptype_all;
else
return &ptype_base[ntohs(pt->type) & PTYPE_HASH_MASK];
}
我们整篇文章都以 ETH_P_ALL 为例,但其实有的时候也会有其它情况。在别的情况下可能会注册协议到 ptype_base 里了,而不是 ptype_all。同样, ptype_base 中的协议也会在发送和接收的过程中被执行到。
总结:tcpdump 启动的时候内部逻辑其实很简单,就是在 ptype_all 中注册了一个虚拟协议而已。
现在我们再回头看开篇提到的几个问题。
用户态 tcpdump 命令是通过 socket 系统调用,在内核源码中用到的 ptype_all 中挂载了函数钩子上去。无论是在网络包接收过程中,还是在发送过程中,都会在网络设备层遍历 ptype_all 中的协议,并执行其中的回调。tcpdump 命令就是基于这个底层原理来工作的。
关于这个问题,得分接收和发送过程分别来看。在网络包接收的过程中,由于 tcpdump 近水楼台先得月,所以完全可以捕获到命中 netfilter 过滤规则的包。
但是在发送的过程中,恰恰相反。网络包先经过协议层,这时候被 netfilter 过滤掉的话,底层工作的 tcpdump 还没等看见就啥也没了。
如果你想自己写一段类似 tcpdump 的抓包程序的话,使用 packet socket 就可以了。我用 c 写了一段抓包,并且解析源 IP 和目的 IP 的简单 demo。
源码地址:https://github.com/yanfeizhang/coder-kung-fu/blob/main/tests/network/test04/main.c
编译一下,注意运行需要 root 权限。
# gcc -o main main.c
# ./main
运行结果预览如下。
我之前说过,YAML 太过复杂,复杂到根本不适合用来作为配置文件。应该是有很多人和我一样想,然后就设计了 TOML 语言。
TOML 就是 Tom's Obvious Minimal Language 的缩写,翻译过来就是汤姆的最精简语言(作者全名 Tom Preston-Werner)。
然后,Slogan 就是 A config file format for humans
也就是:以人为本的配置文件格式。
它的定位已经很明确了吧!
PS: 据说这个语言来自 GitHub(作者的另一个身份是 GitHub 联合创始人)。
其语法说明就只有一个不长的单页面:https://toml.io/cn/v1.0.0。
其语法 ABNF 文件,算上注释和空行,一共 243 行。
printf
格式化limits.h
中的极限值int
, _Bool
(C99, int
别名), bool
/true
/false
(stdbool.h
)l
, ll
, u
, f
)stdint.h
)assert.h
ctype.h
errno.h
float.h
inttypes.h
iso646.h
limits.h
locale.h
math.h
signal.h
stdint.h
stdlib.h
stdio.h
string.h
time.h
wchar.h
wctype.h
// 数据类型 12
short int long double float
char struct union enum typedef
signed unsigned
// 变量 6
auto register extern const volatile
static
// 函数 2
void return
// 控制语句 11
if else for continue while do
switch case default goto break
// 其他 1
sizeof
short
, int
, long
, long long
signed
, unsigned
float
, double
char
, char *
#include <stdio.h>
int main(){
printf("size of int: %2ld\n", sizeof(int));
printf("size of short: %2ld\n", sizeof(short));
printf("size of long: %2ld\n", sizeof(long));
printf("size of long long: %2ld\n", sizeof(long long));
printf("size of float: %2ld\n", sizeof(float));
printf("size of double: %2ld\n", sizeof(double));
printf("size of long double: %2ld\n", sizeof(long double));
printf("size of char: %2ld\n", sizeof(char));
}
$ uname -a
Linux dell 5.11.0-34-generic #36-Ubuntu SMP Thu Aug 26 19:22:09 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux
$ gcc --version
gcc (Ubuntu 10.3.0-1ubuntu1) 10.3.0
Copyright (C) 2020 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
$ gcc /tmp/test.c -o /tmp/test && /tmp/test
size of int: 4
size of short: 2
size of long: 8
size of long long: 8
size of float: 4
size of double: 8
size of long double: 16
size of char: 1
作者可能比较喜欢 C# (C# 的特性你让我丢掉哪一个我都觉得少块肉
), 对 Golang 进行了一些批评,认为其设计缺乏远见,存在很多缺陷:
Anders Hejlsberg 和 Microsoft 把最佳设计都端到眼前了,其他语言纷纷各取所需,但是 Golang 的设计者却不为所动。
今天这一期《科技爱好者周刊》有两个观点,值得聊聊。
1. 全能程序员 vs 特长程序员
2. 算法裁员
dict
INIT_TYPE(&PyDict_Type, "dict");
SETBUILTIN("dict", &PyDict_Type);
PyTypeObject PyDict_Type
的定义在 Objects/dictobject.c
中。
Include/cpython/dictobject.h
typedef struct {
PyObject_HEAD
/* Number of items in the dictionary */
Py_ssize_t ma_used;
/* Dictionary version: globally unique, value change each time
the dictionary is modified */
uint64_t ma_version_tag;
PyDictKeysObject *ma_keys;
/* If ma_values is NULL, the table is "combined": keys and values
are stored in ma_keys.
If ma_values is not NULL, the table is splitted:
keys are stored in ma_keys and values are stored in ma_values */
PyObject **ma_values;
} PyDictObject;
setdefault
#define DICT_SETDEFAULT_METHODDEF \
{"setdefault", (PyCFunction)(void(*)(void))dict_setdefault, METH_FASTCALL, dict_setdefault__doc__},
static PyObject *
dict_setdefault(PyDictObject *self, PyObject *const *args, Py_ssize_t nargs)
{
PyObject *return_value = NULL;
PyObject *key;
PyObject *default_value = Py_None;
if (!_PyArg_CheckPositional("setdefault", nargs, 1, 2)) {
goto exit;
}
key = args[0];
if (nargs < 2) {
goto skip_optional;
}
default_value = args[1];
skip_optional:
return_value = dict_setdefault_impl(self, key, default_value);
exit:
return return_value;
}
static PyObject *
dict_setdefault_impl(PyDictObject *self, PyObject *key,
PyObject *default_value)
/*[clinic end generated code: output=f8c1101ebf69e220 input=0f063756e815fd9d]*/
{
PyObject *val;
val = PyDict_SetDefault((PyObject *)self, key, default_value);
Py_XINCREF(val);
return val;
}
最近发生的一件事情挺有意思的,在得到 “上门自取” 的回复之后,有位美女替外国程序员依照 GPL 协议上门向国内的一家厂商索要内核代码。
深圳公司 UMIDIGI 生产安卓手机(联发科 Mediatek),面向海外用户。
波兰开发者 Patrycja (@ptrcnull) 想将移植 postmarketOS (基于 Alphine Linux) 到 UMIDIGI 的一款设备上,结果发现缺少 ft8719_dsi_fhdplus
屏幕驱动。
Patrycja 尝试联系 UMIDIGI,得到了以下回复:
就是说,如果你要的话,你可以上门来取。我们可以认为这是想让 Patrycja 知难而退。
可是 Patrycja 八月 17 号在 Twitter 上抱怨之后,深圳科技博主 Naomi Wu 机械妖姬(@RealSexyCyborg,混 Youtube)主动提供帮忙,表示愿意代替 Patrycja 上门索取源代码。
然后机械妖姬 08/20 就拿着自拍杆勇闯 UMIDIGI 公司。
UMIDIGI 行政人员表示需要之前发邮件的 BEN 已经离职(我觉得很可能就坐在旁边看美女),然后提供源代码的事情需要先向老板请示,之后在相对友好的氛围下,机械妖姬离开了。
随后:
这件事件我的评价是:
UMIDIGI 之前的邮件回复十分愚蠢,直接向法务部门咨询之后,通过合法的途径(可能涉及联发科的知识产权)将代码提供给他不就好了吗?
不过,如果不是事情闹大了,UMIDIGI 会不会提供源代码呢?这就不知道了,我们也不能以恶意揣度之。只能就事论事,在这次事件中,各方的表现都非常好。
美国之声中文网 @VOAChinese
百万阿富汗难民主要流向巴基斯坦、伊朗,世界多国纷纷提供人道庇护,但并不包括中国。美国尚未宣布接受难民的确切数字,总统拜登批准了5亿美元应急资金,而美国北方司令部正为多达 5万阿富汗撤离人员提供交通、医疗、临时住房和其他支持,协助他们安顿到永久或临时设施中,多个州也参与到安置工作中。
瞧瞧这个 “并不包括中国”,cue 得多自然,值得我们的新闻工作者学习。