操作系统有很多问题出现在触摸事件上,而平时自己手上只有鼠标控制,突发奇想,如果我将鼠标事件虚拟成触摸,那么我复现问题,调试代码不是很方便么。
uinput是用户空间的输入设备驱动,它提供了一种功能将内核的输入设备搬到应用层来模拟。内核通过如下配置打开
CONFIG_INPUT_UINPUT=y
鉴于我们的系统目前默认是x11环境,所以我们需要使用xlib的XQueryPointer函数,此函数可以获取X下的鼠标指针位置,XQueryPointer的原型如下
Bool XQueryPointer(Display *display, Window w, Window *root_return, Window *child_return, int *root_x_return, int *root_y_return, int *win_x_return, int *win_y_return, unsigned int *mask_return);
根据上面的信息,如果我们声明uinput是一个输入设备,然后通过XQueryPointer拿到鼠标在X的位置后,将其传给uinput,那么我们在系统中就白白获得了一个虚假的触摸设备。代码如下
#include <stdio.h> #include <pthread.h> #include <stdlib.h> #include <string.h> #include <time.h> #include <fcntl.h> #include <unistd.h> #include <linux/uinput.h> #include <X11/Xlib.h> void emit(int fd, int type, int code, int val) { struct input_event ie; ie.type = type; ie.code = code; ie.value = val; ie.time.tv_sec = 0; ie.time.tv_usec = 0; write(fd, &ie, sizeof(ie)); } int main(void) { struct uinput_setup usetup; struct uinput_abs_setup uabs; int fd = open("/dev/uinput", O_WRONLY | O_NONBLOCK); ioctl(fd, UI_SET_EVBIT, EV_KEY); ioctl(fd, UI_SET_KEYBIT, BTN_TOUCH); ioctl(fd, UI_SET_EVBIT, EV_REL); ioctl(fd, UI_SET_RELBIT, REL_X); ioctl(fd, UI_SET_RELBIT, REL_Y); ioctl(fd, UI_SET_EVBIT, EV_ABS); ioctl(fd, UI_SET_ABSBIT, ABS_MT_POSITION_X); ioctl(fd, UI_SET_ABSBIT, ABS_MT_POSITION_Y); ioctl(fd, UI_SET_ABSBIT, ABS_MT_SLOT); ioctl(fd, UI_SET_ABSBIT, ABS_MT_TRACKING_ID); memset(&usetup, 0, sizeof(usetup)); usetup.id.bustype = BUS_USB; usetup.id.vendor = 0x1234; usetup.id.product = 0x5678; strcpy(usetup.name, "kylin virtual touch device"); ioctl (fd, UI_SET_PROPBIT, INPUT_PROP_DIRECT); memset(&uabs, 0, sizeof(uabs)); uabs.code = ABS_MT_SLOT; uabs.absinfo.minimum = 0; uabs.absinfo.maximum = 9; ioctl(fd, UI_ABS_SETUP, &uabs); memset(&uabs, 0, sizeof(uabs)); uabs.code = ABS_MT_POSITION_X; uabs.absinfo.minimum = 0; uabs.absinfo.maximum = 1920; uabs.absinfo.resolution= 76; ioctl(fd, UI_ABS_SETUP, &uabs); memset(&uabs, 0, sizeof(uabs)); uabs.code = ABS_MT_POSITION_Y; uabs.absinfo.minimum = 0; uabs.absinfo.maximum = 1200; uabs.absinfo.resolution= 106; ioctl(fd, UI_ABS_SETUP, &uabs); ioctl(fd, UI_DEV_SETUP, &usetup); ioctl(fd, UI_DEV_CREATE); Display *display; Window root; Window child; int root_x, root_y, win_x, win_y; unsigned int mask; display = XOpenDisplay(NULL); if (display == NULL) { return 1; } root = DefaultRootWindow(display); /* * On UI_DEV_CREATE the kernel will create the device node for this * device. We are inserting a pause here so that userspace has time * to detect, initialize the new device, and can start listening to * the event, otherwise it will not notice the event we are about * to send. This pause is only needed in our example code! */ sleep(1); int mouse_fd = open("/dev/input/event4", O_RDONLY); if (mouse_fd < 0) { ioctl(fd, UI_DEV_DESTROY); close(fd); return EXIT_FAILURE; } while (1) { struct input_event ie; read(mouse_fd, &ie, sizeof(ie)); if (ie.type == EV_REL && (ie.code == REL_X || ie.code == REL_Y)) { continue; } switch (ie.code){ case BTN_LEFT: if(ie.value==1){ XQueryPointer(display, root, &root, &child, &root_x, &root_y, &win_x, &win_y, &mask); emit(fd, EV_ABS, ABS_MT_TRACKING_ID, 1); emit(fd, EV_ABS, ABS_MT_POSITION_X, root_x); emit(fd, EV_ABS, ABS_MT_POSITION_Y, root_y); emit(fd, EV_KEY, BTN_TOUCH, 1); emit(fd, EV_SYN, SYN_REPORT, 0); }else{ emit(fd, EV_ABS, ABS_MT_TRACKING_ID, -1); emit(fd, EV_KEY, BTN_TOUCH, 0); emit(fd, EV_SYN, SYN_REPORT, 0); } default: break; } } /* * Give userspace some time to read the events before we destroy the * device with UI_DEV_DESTOY. */ sleep(1); XCloseDisplay(display); ioctl(fd, UI_DEV_DESTROY); close(fd); return 0; }
值得注意的是,因为我们插入的鼠标的事件是不太确定的,所以需要提前手动查询一下,如下
先通过lsusb查看鼠标设备
# lsusb ...... Bus 001 Device 003: ID 25a7:fa61 Compx 2.4G Receiver
然后通过proc下的信息找到对应的事件
# cat /proc/bus/input/devices ...... I: Bus=0003 Vendor=25a7 Product=fa61 Version=0110 N: Name="Compx 2.4G Receiver Mouse" P: Phys=usb-fc800000.usb-1.2/input1 S: Sysfs=/devices/platform/fc800000.usb/usb1/1-1/1-1.2/1-1.2:1.1/0003:25A7:FA61.0002/input/input4 U: Uniq= H: Handlers=event4 B: PROP=0 B: EV=17 B: KEY=1f0000 0 0 0 0 B: REL=1943 B: MSC=10
此时我们知道事件是event4,那么代码填入的就是event4如下
int mouse_fd = open("/dev/input/event4", O_RDONLY);
此时编译上述代码如下
gcc mouse2touch.c -lX11 -pthread -o mouse2touch
如果我们在机器内运行,因为会XOpenDisplay,所以需要DISPLAY的环境变量设置正确,默认可以设置如下
export DISPLAY=:0
此时直接运行即可
root@kylin:~# ./mouse2touch
注意此程序是daemon进程,会while 1监听事件,如果突然退出需要检查是否有其他异常
当程序运行后,我们看到内核会有如下日志
input: kylin virtual touch device as /devices/virtual/input/input28
然后我们查看事件也能在/proc/bus/input/devices存在如下
I: Bus=0003 Vendor=1234 Product=5678 Version=0000 N: Name="kylin virtual touch device" P: Phys= S: Sysfs=/devices/virtual/input/input28 U: Uniq= H: Handlers=event18 B: PROP=2 B: EV=f B: KEY=400 0 0 0 0 0 B: REL=3 B: ABS=260800000000000
同样的Xorg也能正常识别,所以Xorg也有如下日志
tail -f /var/log/Xorg.0.log (II) event18 - kylin virtual touch device: is tagged by udev as: Touchscreen (II) event18 - kylin virtual touch device: device is a touch device
正好对应上了。此时我们点击鼠标,可以看到有触摸的特效出现
可以发现功能得到实现了,至此已经完全满足我在不需要触摸屏的前提下调试触摸的操作系统BUG了。
上面通过编写了一个小程序,实现了鼠标模拟触摸的情况,但是还是有一些小缺点的,主要如下
根据上面的信息,我简单的利用了uinput来辅助调试操作系统触摸问题,稍微扩展一下,我留个疑问,有兴趣可以思考一下。
双向链表是刚毕业就基于linux的实现了解过,后面因为内核的实现没办法直接搬出来用,所以自己应用开发的时候借鉴linux的实现重写了一份,这样方便自己粘贴到任意代码上。
时间长了,关于双向链表的实现很容易印象不深刻,这时候别人突然一问,让我写一个双向链表出来,我能给出思路,但是没办法完美写出来。
举个例子,我们每个人都学过关雎,里面一句"窈窕淑女,君子好逑"可谓人尽皆知,但突然某一天,有人让你背诵关雎,你会不会发懵呢?
本文基于linux内核的实现做一下分析,然后将自己的实现粘贴进来,主要目的是做一个复习。如有有喜欢我的实现的,放心拿去用好啦。
在内核中list.h实现了双向链表,其结构如下
struct list_head { struct list_head *next, *prev; };
doubly linked list的创建就是将自己指向自己。所以有如下宏
#define LIST_HEAD_INIT(name) { &(name), &(name) } #define LIST_HEAD(name) \ struct list_head name = LIST_HEAD_INIT(name)
这样传入list_head结构体就可以初始化一个doubly linked list。
内核还提供了一个函数实现INIT_LIST_HEAD来实现初始化
static inline void INIT_LIST_HEAD(struct list_head *list) { WRITE_ONCE(list->next, list); list->prev = list; }
对于链表的插入需要区分头插还是尾插。那么add有两个实现 list_add和list_add_tail。内核将其头插和尾插抽象成形参传递了,如下
static inline void __list_add(struct list_head *new, struct list_head *prev, struct list_head *next)
这里如果是头插,那么组成关系如下
head ---> new ---> head->next
如果是尾插,那么组成关系如下
head->prev ---> new ---> head
所以__list_add的实现只需要关心位于new的prev和next的位置,那么list_add和list_add_tail只是不同的封装而已,考一下大家,下面哪个是list_add,哪个是list_add_tail。
__list_add(new, head, head->next); // 1 __list_add(new, head->prev, head); // 2
关于__list_add的实现就是
值得注意的是,针对prev->next使用了WRITE_ONCE,这样使得prev->next会保证赋值的有效性。 所以我们看内核实现如下
static inline void __list_add(struct list_head *new, struct list_head *prev, struct list_head *next) { next->prev = new; new->next = next; new->prev = prev; WRITE_ONCE(prev->next, new); }
内核关于list_add和list_add_tail的实现如下
static inline void list_add(struct list_head *new, struct list_head *head) { __list_add(new, head, head->next); } static inline void list_add_tail(struct list_head *new, struct list_head *head) { __list_add(new, head->prev, head); }
对于删除节点而言,我们只需要知道被删除节点即可。然后如下
对于内核而言,其实现也做了抽象。我们知道待删除节点假设是entry,那么__list_del提供两个参数,一个是prev,一个是next。这样对于删除而言就是简单的如下
那么内核实现如下
static inline void __list_del(struct list_head * prev, struct list_head * next) { next->prev = prev; WRITE_ONCE(prev->next, next); }
当内核实现了__list_del
,那么关于删除函数__list_del_entry
就是简单的将形参entry拆分成entry->prev和entry->next。如下
static inline void __list_del_entry(struct list_head *entry) { __list_del(entry->prev, entry->next); }
基于上面,为了满足调试需求,内核故意对被删除的节点设置了固定的值,这样使用crash等工具就能很方便的判断问题了。如下
static inline void list_del(struct list_head *entry) { __list_del_entry(entry); entry->next = LIST_POISON1; entry->prev = LIST_POISON2; } define LIST_POISON1 ((void *) 0x100 + POISON_POINTER_DELTA) #define LIST_POISON2 ((void *) 0x122 + POISON_POINTER_DELTA) CONFIG_ILLEGAL_POINTER_VALUE=0xdead000000000000
我们组合一下上面值可以得到如下
0xdead000000000100 0xdead000000000122
我们经常使用crash的同学可以看到如下信息
list = { next = 0xdead000000000100, prev = 0xdead000000000122 }
替换函数的主要思路为
也就是说先针对new自身的next,设置为old的next,同时修复原old的next的prev改变成new自己。
然后再针对new自身的prev,设置为old的prev,同时修复原old的prev的next设置为自己
所以替换的实现如下
static inline void list_replace(struct list_head *old, struct list_head *new) { new->next = old->next; new->next->prev = new; new->prev = old->prev; new->prev->next = new; }
交换的目标是对entry1和entry2的内容在链表进行交换。 那么思路应该如下
根据上面的分析,以及以上章节的代码展示,可以默写步骤如下
但是上面可能在一种情况下出bug,那就是当entry2->prev
就是entry1的时候,此时对entry2->prev
的头插,就是对entry1的头插,但是list_replace已经修改了entry1的值变成实际entry2的实际值,所以entry2->prev已经不是属于这个doubly link list中了,所以我们更新为entry2。代码如下
static inline void list_swap(struct list_head *entry1, struct list_head *entry2) { struct list_head *pos = entry2->prev; list_del(entry2); list_replace(entry1, entry2); if (pos == entry1) pos = entry2; list_add(entry1, pos); }
上面如果理解不太清楚,下面我画图解释如下。最开始链表如下
---> entry1(pos) ---> entry2 ---->
当删除entry2时,如下
---> entry1(pos) ---->
当替换entry1的之后如下
---> entry2 ---->
此时我们需要完成步骤4,但是此时entry1也就是pos不在这个双向链表中了。所以我们需要强制修改pos为entry2如下
---> entry2(pos) ---->
然后调用list_add,结果如下
---> entry2(pos) ----> entry1 ---->
很明显,如果entry1不是entry2的prev,那么就无需关心此问题
移动非常好理解,就是将某个元素删除后,然后通过__list_add
添加,因为__list_add
有两个实现,所以move有两个版本也就是list_move
和list_move_tail
。其实现如下
static inline void list_move(struct list_head *list, struct list_head *head) { __list_del_entry(list); list_add(list, head); } static inline void list_move_tail(struct list_head *list, struct list_head *head) { __list_del_entry(list); list_add_tail(list, head); }
关于拼接,__list_splice
也做了抽象,这样便于理解,所以有三个形参如下
static inline void __list_splice(const struct list_head *list, struct list_head *prev, struct list_head *next)
这里list是待拼接的链表,而prev和next是需要拼接的位置的prev和next。
对于拼接而言,其步骤如下
上面需要进一步解释,这里我们因为待拼接的list是整个list,所以我们知道如下
经过这样进一步的解释,再对照内核如下代码
static inline void __list_splice(const struct list_head *list, struct list_head *prev, struct list_head *next) { struct list_head *first = list->next; struct list_head *last = list->prev; first->prev = prev; prev->next = first; last->next = next; next->prev = last; }
我们可以清楚的知道了拼接的实现
内核提供了list_entry实现,其实就是container_of。关于container_of这里不重复了,可以查阅文章《内核C语言结构体定位成员》
对于双向循环链表的遍历,可以分为根据next遍历,还是根据prev遍历,所以有两个变体,如下
#define list_for_each(pos, head) \ for (pos = (head)->next; pos != (head); pos = pos->next) #define list_for_each_prev(pos, head) \ for (pos = (head)->prev; pos != (head); pos = pos->prev)
根据遍历的代码,可以看到是通过for循环,从head处一直找next/prev,直到pos再次等于head的时候停止遍历。但是这里实际上是不安全的。也是很多代码导致崩溃的原因,本人遇到未使用安全遍历导致遍历崩溃的问题已经数十次有余了。
其原因是这个list的当前值,会在for语句里面可能被删除,例如在list_for_each的里面,会调用list_del(pos)把当前pos删掉。针对这种情况,需要引入一个临时遍历n,它保存了pos->next的值。这样安全的意思就是在遍历的过程中,可以放心的删除pos。其代码实现如下
#define list_for_each_prev_safe(pos, n, head) \ for (pos = (head)->prev, n = pos->prev; \ pos != (head); \ pos = n, n = pos->prev)
我们直到container_of的作用是获取list的上层结构体,而遍历的作用是从头遍历list所有成员。
那么遍历entry的含义就是两者的结合版本,通过list_for_each遍历所有list,然后通过entry获取上层结构体,其代码如下
#define list_for_each_entry(pos, head, member) \ for (pos = list_first_entry(head, typeof(*pos), member); \ !list_entry_is_head(pos, head, member); \ pos = list_next_entry(pos, member))
可以看到这里的pos不再是list某个成员,而是所带list的上层结构体entry了。
遍历entry也有安全函数版本,如果在循环内部需要删除pos,那么请使用安全版本,其代码如下
#define list_for_each_entry_safe(pos, n, head, member) \ for (pos = list_first_entry(head, typeof(*pos), member), \ n = list_next_entry(pos, member); \ !list_entry_is_head(pos, head, member); \ pos = n, n = list_next_entry(n, member))
上面把内核的实现捋了一遍,我又翻箱倒柜找到了自己当时list.h的实现。这里也简单分析一下。
自己的list实现思路是参考了内核,但没内核写的高级,纯粹按照个人思路完成。本人多个工程都用此list.h文件,目前没什么问题。最后贴上list.h的个人版本所有代码
结构体和内核一致
struct list_head { struct list_head *prev; struct list_head *next; };
和内核一致,但是不考虑变量的原子性
static inline void list_inithead(struct list_head *item) { item->prev = item; item->next = item; }
添加的两个版本没有内核的抽象,我的思路如下 首先默认情况如下
list<---> A
此时添加item,首先先把item的prev和next接到list和A中间
list<---item--->A
item->prev = list; item->next = list->next;
此时A的prev是断开的,list的next也是断开的,修复doubly link list关系
list<--->item<--->A
list->next->prev = item; list->next = item;
故代码如下
static inline void list_add(struct list_head *item, struct list_head *list) { item->prev = list; item->next = list->next; list->next->prev = item; list->next = item; }
tail的版本是添加到list之前,假设默认情况如下
A<--->list
此时添加item,首先先把item的prev和next接到list和A中间
A<---item--->list
item->next = list; item->prev = list->prev;
此时A是list->prev,其next是断开的,list的prev也是断开的,修复一下
A<--->item<--->list
list->prev->next = item; list->prev = item;
故代码如下
static inline void list_addtail(struct list_head *item, struct list_head *list) { item->next = list; item->prev = list->prev; list->prev->next = item; list->prev = item; }
首先默认情况如下
list<--->item<--->B
我们幻想一个删除后的结果如下
list<---?--->B
所以在item角度修复其item->prev->next
和item->next->prev
list<--->B
item->prev->next = item->next; item->next->prev = item->prev;
个人的实现没有内核的标记,所有直接置空即可,故整体代码如下
static inline void list_del(struct list_head *item) { item->prev->next = item->next; item->next->prev = item->prev; item->prev = item->next = NULL; }
在解决《glibc的chunk破坏问题》问题的时候,我特地实现了validate函数,此函数判断doubly link list的每个成员是否正常,其思路是
static inline void list_validate(const struct list_head *list) { struct list_head *node; assert(list_is_linked(list)); assert(list->next->prev == list && list->prev->next == list); for (node = list->next; node != list; node = node->next) assert(node->next->prev == node && node->prev->next == node); }
拼接的思路上文已经叙述清楚了,这里绘图示意即可。假设默认两个list如下
tail(A)<--->src<--->A dst<--->B
第一步,将src拆开,将A的prev挂到dst,将tail的next挂到B上。为了简化,假设tail就是A。
dst<---A(tail)--->B
src->next->prev = dst; src->prev->next = dst->next;
第二步,将B的prev设置为tail(A)
dst<---A(tail)<--->B
dst->next->prev = src->prev;
第三步,将dst的next设置为A
dst<--->A(tail)<--->B
dst->next = src->next;
此时拼接完成,故整体代码如下
static inline void list_splice(struct list_head *src, struct list_head *dst) { if (list_is_empty(src)) return; src->next->prev = dst; src->prev->next = dst->next; dst->next->prev = src->prev; dst->next = src->next; }
拼接尾的思路完全一致,不同的是拼接到head前面。假设默认两个list如下
tail(A)<--->src<--->A tail(B)<--->dst<--->B
第一步,将src拆开,将tail(A)的next挂到dst,将A的prev挂到tail(B)上。
tail(B)<---tail(A)--->dst
src->prev->next = dst; src->next->prev = dst->prev;
第二步,将tail(B)的next设置为A
tail(B)<--->tail(A)--->dst
dst->prev->next = src->next;
第三步,将dst的prev设置为tail(A)
tail(B)<--->tail(A)--->dst
dst->prev = src->prev;
值得注意的是,如果此链表只要一个元素A/B,那么tail(A)就是A,tail(B)就是B,如果是多个元素,你也可以理解tail(A)是src的最后一个元素,tail(B)是dest的最后一个元素。
关于list_entry的宏定义,可以直接从内核抄,只不过我们需要替换成标准的offsetof实现,如下
#define list_entry(__item, __type, __field) \ ((__type *)(((char *)(__item)) - offsetof(__type, __field)))
在此之上list_for_each
和list_for_each_entry
的实现可以无缝复制粘贴
当然,其safe实现也可以无缝粘贴。
#ifndef _LIST_H_ #define _LIST_H_ #include <stdbool.h> #include <stddef.h> #include <assert.h> #define list_assert(cond, msg) assert(cond && msg) struct list_head { struct list_head *prev; struct list_head *next; }; static inline void list_inithead(struct list_head *item) { item->prev = item; item->next = item; } static inline void list_add(struct list_head *item, struct list_head *list) { item->prev = list; item->next = list->next; list->next->prev = item; list->next = item; } static inline void list_addtail(struct list_head *item, struct list_head *list) { item->next = list; item->prev = list->prev; list->prev->next = item; list->prev = item; } static inline bool list_is_empty(const struct list_head *list) { return list->next == list; } static inline void list_replace(struct list_head *from, struct list_head *to) { if (list_is_empty(from)) { list_inithead(to); } else { to->prev = from->prev; to->next = from->next; from->next->prev = to; from->prev->next = to; } } static inline void list_del(struct list_head *item) { item->prev->next = item->next; item->next->prev = item->prev; item->prev = item->next = NULL; } static inline void list_delinit(struct list_head *item) { item->prev->next = item->next; item->next->prev = item->prev; item->next = item; item->prev = item; } static inline bool list_is_linked(const struct list_head *list) { assert((list->prev != NULL) == (list->next != NULL)); return list->next != NULL; } static inline bool list_is_singular(const struct list_head *list) { return list_is_linked(list) && !list_is_empty(list) && list->next->next == list; } static inline unsigned list_length(const struct list_head *list) { struct list_head *node; unsigned length = 0; for (node = list->next; node != list; node = node->next) length++; return length; } static inline void list_splice(struct list_head *src, struct list_head *dst) { if (list_is_empty(src)) return; src->next->prev = dst; src->prev->next = dst->next; dst->next->prev = src->prev; dst->next = src->next; } static inline void list_splicetail(struct list_head *src, struct list_head *dst) { if (list_is_empty(src)) return; src->prev->next = dst; src->next->prev = dst->prev; dst->prev->next = src->next; dst->prev = src->prev; } static inline void list_validate(const struct list_head *list) { struct list_head *node; assert(list_is_linked(list)); assert(list->next->prev == list && list->prev->next == list); for (node = list->next; node != list; node = node->next) assert(node->next->prev == node && node->prev->next == node); } static inline void list_move_to(struct list_head *item, struct list_head *loc) { list_del(item); list_add(item, loc); } #define list_entry(__item, __type, __field) \ ((__type *)(((char *)(__item)) - offsetof(__type, __field))) #define list_container_of(ptr, sample, member) \ (void *)((char *)(ptr) \ - ((char *)&(sample)->member - (char *)(sample))) #define list_first_entry(ptr, type, member) \ list_entry((ptr)->next, type, member) #define list_last_entry(ptr, type, member) \ list_entry((ptr)->prev, type, member) #define LIST_FOR_EACH_ENTRY(pos, head, member) \ for (pos = NULL, pos = list_container_of((head)->next, pos, member); \ &pos->member != (head); \ pos = list_container_of(pos->member.next, pos, member)) #define LIST_FOR_EACH_ENTRY_SAFE(pos, storage, head, member) \ for (pos = NULL, pos = list_container_of((head)->next, pos, member), \ storage = list_container_of(pos->member.next, pos, member); \ &pos->member != (head); \ pos = storage, storage = list_container_of(storage->member.next, storage, member)) #define LIST_FOR_EACH_ENTRY_SAFE_REV(pos, storage, head, member) \ for (pos = NULL, pos = list_container_of((head)->prev, pos, member), \ storage = list_container_of(pos->member.prev, pos, member); \ &pos->member != (head); \ pos = storage, storage = list_container_of(storage->member.prev, storage, member)) #define LIST_FOR_EACH_ENTRY_FROM(pos, start, head, member) \ for (pos = NULL, pos = list_container_of((start), pos, member); \ &pos->member != (head); \ pos = list_container_of(pos->member.next, pos, member)) #define LIST_FOR_EACH_ENTRY_FROM_REV(pos, start, head, member) \ for (pos = NULL, pos = list_container_of((start), pos, member); \ &pos->member != (head); \ pos = list_container_of(pos->member.prev, pos, member)) #define list_for_each_entry(type, pos, head, member) \ for (type *pos = list_entry((head)->next, type, member), \ *__next = list_entry(pos->member.next, type, member); \ &pos->member != (head); \ pos = list_entry(pos->member.next, type, member), \ list_assert(pos == __next, "use _safe iterator"), \ __next = list_entry(__next->member.next, type, member)) #define list_for_each_entry_safe(type, pos, head, member) \ for (type *pos = list_entry((head)->next, type, member), \ *__next = list_entry(pos->member.next, type, member); \ &pos->member != (head); \ pos = __next, \ __next = list_entry(__next->member.next, type, member)) #define list_for_each_entry_rev(type, pos, head, member) \ for (type *pos = list_entry((head)->prev, type, member), \ *__prev = list_entry(pos->member.prev, type, member); \ &pos->member != (head); \ pos = list_entry(pos->member.prev, type, member), \ list_assert(pos == __prev, "use _safe iterator"), \ __prev = list_entry(__prev->member.prev, type, member)) #define list_for_each_entry_safe_rev(type, pos, head, member) \ for (type *pos = list_entry((head)->prev, type, member), \ *__prev = list_entry(pos->member.prev, type, member); \ &pos->member != (head); \ pos = __prev, \ __prev = list_entry(__prev->member.prev, type, member)) #define list_for_each_entry_from(type, pos, start, head, member) \ for (type *pos = list_entry((start), type, member); \ &pos->member != (head); \ pos = list_entry(pos->member.next, type, member)) #define list_for_each_entry_from_safe(type, pos, start, head, member) \ for (type *pos = list_entry((start), type, member), \ *__next = list_entry(pos->member.next, type, member); \ &pos->member != (head); \ pos = __next, \ __next = list_entry(__next->member.next, type, member)) #define list_for_each_entry_from_rev(type, pos, start, head, member) \ for (type *pos = list_entry((start), type, member); \ &pos->member != (head); \ pos = list_entry(pos->member.prev, type, member)) #endif
上述代码和内核一样,声明成list.h就可搬到代码使用了。
本文重新分析了内核list的实现,并找到了原来自己写的list.h。可以发现,对于不同的list实现,思路并不是一样的。
我在分析内核的实现的时候,还得停下来想一会儿,才能清楚。但是分析自己原来写的东西的时候,一下就知道当时自己为什么这么写,很快就给出了图示。
所以人的思维还是有一定定式的,如果是内核的实现方式方式,我并不一定能想到并写出来。而自己的实现又没有常常去复习。所以这才是我现场写不出来一个双向循环链表的根本原因吧,自省之~
我们自己使用的服务器,经常需要编译代码等操作,并且是多人并行使用的服务器状态,使用久了的情况下,服务器经常会出现如下问题。
对于我这么多年使用服务器的经验而言,其实一直都是在平衡上面五个问题的状态,让其在上面五个问题中间得到平衡,而不因为某个点导致服务器的性能下降。
可见对于不同场景的服务器,需要为其设置不同的内存调节状态,本文基于编译型服务器,提出关于内存调节的一下想法和见解。这会对使用服务器有更好的理解。
只有拥有一个调教好的服务器,才能发挥利用服务器发挥自己最大的工作效率
linux默认配置swappiness是60,通常情况下,这个值的范围是0-100,值越高,则回收的时候优先选择匿名页,然后将匿名页搬运到swap分区中,值越低,会减少swap的使用,从而由其他参数调节page cache的行为
为什么首先谈这个呢。根据上面理解的,swappiness默认是60,那么从某种平衡上来看,如果系统存在内存压力的情况下,会稍微将anon page移到swap 分区中。那么将anon page移到swap 分区因为IO带来的性能损失和延迟对绝大部分人是能够接收的,这样就不至于可能出现系统完全无内存导致的oom情况。
根据默认值60,我们知道,linux内核考虑了很通用的情况下,牺牲一些性能从而满足内存在压力情况下的可用。
而面对服务器而言,特别是编译型服务器,我们每个编译任务可能是长时间,但一定会完成的,所以不希望移到swap分区,如果维持在60的值,大型任务例如llvm,chromium的构建就会很慢。所以需要降低swappiness的值
那我们设置swappiness的值是0还是1呢。根据长时间的经验来看,总结如下
可以看到,根据使用经验而言,swappiness在编译型服务器上,应该很低,但是如果设置为0,那么swap分区即使设置了很大,内核还是不会使用swap分区的空间,而是直接oom。那么推荐swappiness的值是1。当内存不够的时候,才将swap分区利用起来
根据swappiness的配置,我们能够解决默认情况下编译代码缓慢的问题,因为系统不再将anon page移动到swap 分区中了,也不会突然因为不当的swappiness设置导致oom情况出现,编译速度得到了加快。
根据上面的设置,系统默认不使用swap分区了,那么所有的任务都更倾向于使用内存了,对于很多人而言,都了解一个配置 drop_caches ,认为drop_caches能够通过定期任务减缓内存压力。
按照本人理解,其实 drop_caches 是清空page cache和slab cache
如果我们echo 1 > /proc/sys/vm/drop_caches
那么情况的是page cache。 具体而言是清空处于inactive链表的page cache,实验如下:
未清空page cache如下
root@kylin:~# cat /proc/meminfo | grep -i active Active: 1361888 kB Inactive: 46626572 kB Active(anon): 843576 kB Inactive(anon): 387512 kB Active(file): 518312 kB Inactive(file): 46239060 kB
此时我们运行# echo 1 > /proc/sys/vm/drop_caches
得到信息如下
root@kylin:~# cat /proc/meminfo | grep -i active Active: 1023844 kB Inactive: 438784 kB Active(anon): 862328 kB Inactive(anon): 368504 kB Active(file): 161516 kB Inactive(file): 170280 kB
可以看到,处于inactive上的46G内存得到了释放
接下来看drop_caches对slab的清空情况。首先我们知道slab的值是SReclaimable+SUnreclaim。而同样的drop_caches针对的是可回收的内存,也就是SReclaimable,而不是不可回收的内存SUnreclaim,演示如下
root@kylin:~# cat /proc/meminfo | grep -i slab -A 2 Slab: 3724284 kB SReclaimable: 3116412 kB SUnreclaim: 607872 kB root@kylin:~# echo 2 > /proc/sys/vm/drop_caches root@kylin:~# cat /proc/meminfo | grep -i slab -A 2 Slab: 602276 kB SReclaimable: 115296 kB SUnreclaim: 486980 kB
可以看到,对于SReclaimable对于的内存,得到了回收。
实际上,对于编译型服务器,这里更多是dentry cache,也就是文件系统访问缓存,我们演示一下如下
# cat /proc/sys/fs/dentry-state 97122 77478 45 0 53596 0 # echo 2 > /proc/sys/vm/drop_caches && cat /proc/sys/fs/dentry-state 17018 23 45 0 1065 0
可以看到,nr_unused从7万变为23了,内存从这里得到了释放
针对此,很多人,包括早期我自己也很喜欢定期执行 sync和drop_cachessync && echo 1 > /proc/sys/vm/drop_caches
。 sync的目的是将脏页写回,这样可回收的内存就会变多。但是根据上面的了解,其缺点也很明显,针对编译型服务器,其dentry cache得到了错误的清除,导致编译速度变慢了,不仅如此,服务器还会出现概率卡死。
但实际上,如果我们了解内存的配置,其实无需定期执行,而是由kernel的self tuning。会比定期执行更好。
根据上面提到的drop_caches,我们先讨论page caches的回收,这里借助lorenzo stoakes编写《the linux memory manager》一张图如下
根据此图,可以看到很熟悉的一个机制,那就是linux 水位机制,这里简单回顾一下
可以知道,如果内存压力太大,那么内核会自己回收内存,而不需要自己定期的drop_caches
其二,对于slab cache的self tuning方式,如下
这里可以看到,slab默认的shrink机制能够自动回收slab cache。其机制也属于关于slab的indirect/direct reclaim回收,所以也符合水位机制。
根据上面介绍的,我们可以针对性的调整水位的比例。
针对编译型服务器,根据多年的使用经验,那么可能出现如下问题
可以看到,默认的配置还是不满足编译型服务器,我们需要针对性的修改sysctl,如下
对于min一下无可用内存,直接oom的情况,我们可以修改min_free_kbytes的值,通常min_free_kbytes的值也是内核通过当前所有zones的总和计算出来的,为了考虑常规情况,此值通常偏低了点,为了应对编译型服务器,那么需要设置高点,多高呢。我们可以对照内核的init_per_zone_wmark_min函数查看
int __meminit init_per_zone_wmark_min(void) { ...... min_free_kbytes = new_min_free_kbytes; if (min_free_kbytes < 128) min_free_kbytes = 128; if (min_free_kbytes > 262144) min_free_kbytes = 262144; ...... return 0; }
可以知道,内核最大可以设置到256M,鉴于我们是服务器,内存本身就很大,所以我们可以设置为256,如下
echo 262144 > /proc/sys/vm/min_free_kbytes
不过这个是根据实际情况,太大了也不一定好,个人建议设置到0.05%。也就是对于128G内存,设置到64M。
设置完了之后,可以长期监听vmstat的kswapd_low_wmark_hit_quickly 来判断你的服务器设置的值是否合理,如下
# cat /proc/vmstat | grep kswapd_low_wmark_hit_quickly kswapd_low_wmark_hit_quickly 358635
这个值代表kswapd到low水位的次数,这里我已经到358635次了。这里值得注意的是,这个次数不是越高越好,也不是越低越好。是根据情况查看增长速率,如下:
这样,我们当内存不够的时候,我们就不会出现无法direct reclaim导致的oom情况了。
如果内存从low到min速度太快,又因为直接回收是阻塞的,所以我们经常发现服务器突然卡死了。 通常这种情况下,如果没有什么重要的事情,那么等一会儿就好了。
直接解决此问题的方式就是增加内存,例如32G的内存可以增加到128G,128G的内存可以增加到256G。
这里讨论的是既不想等一会儿自己恢复,又短时间无法增加服务器内存的情况。
针对这种情况,解决方案也很容易想到,我们扩大low到min的距离,也就是,假设min是64M,那我low是640M行不行呢?
很可惜的是,内核只提供了比例设置,不提供具体的值的设置。我们可以设置watermark_scale_factor来缓解这个问题
watermark_scale_factor控制了kswapd的主动性,默认其分母是10000,其值设置是10,那么是0.1%比率作为计算,也就是high与low的差距是总内存的0.1%。
针对此问题,缓解的方法是设置watermark_scale_factor大一点,具体多大呢,需要根据如下值的增长情况判断,
root@kylin:# cat /proc/vmstat |grep 'allocstall' allocstall_dma32 0 allocstall_normal 173606 allocstall_movable 4338981
这里arm64没有ZONE_DMA,不用奇怪,我们监控normal和movable的allocstall值,如果这个值变大,那么需要将watermark_scale_factor放大。
echo 40 > /proc/sys/vm/watermark_scale_factor
根据经验,调整watermark_scale_factor并不能改善无响应的问题,因为内存不够就是不够,只是改善进入直接回收的时机,也就是扩大水位线之间的差距,从而减少系统无响应的次数
默认情况下,内核设置了overcommit_memory的值是0,意味着如果没有可用内存,则申请内存失败。它会影响到一些大型任务构建是否成功。
也就是说,有时候会因为内存不够,导致某些程序无法编译成功,所以有些情况下,我们需要设置overcommit_memory为1如下
echo 1 > /proc/sys/vm/overcommit_memory
这里含义如下
值得注意的是,如果设置为1,那么可能会导致oom,理由可想而知。
上面已经说明清楚了内核关于内存的回收流程,但服务器的配置光内存的回收还不够,我们需要控制脏页的回收策略。
对于脏页的回收设置,有一个文章推荐设置具体的值,而不是比率。如下
所以我们设置脏页回收策略根据值来配置。
vm.dirty_ratio = 0 vm.dirty_bytes = 629145600 # keep the vm.dirty_background_bytes to approximately 50% of this setting vm.dirty_background_bytes = 314572800
值得注意的是默认情况下,设置的dirty_bytes是0,而dirty_ratio是20。所以对于编译型服务器而言,这里必须修改一下。
上面可以看到slab的SReclaimable回收来源于dentry,因为我们是编译型服务器,所以我们需要调整vfs_cache_pressure。
vfs_cache_pressure的作用是回收文件系统缓存,如果我们内存吃紧,还容易出现OOM,那么就增高此值,如果内存足够,则降低此值。
为了让服务器能够编译,并且保留缓存,我更倾向于降低此值。
echo 50 > /proc/sys/vm/vfs_cache_pressure
如果你编译大型任务,发现出现OOM了,那么就增大此值
echo 500 > /proc/sys/vm/vfs_cache_pressure
至此,我总结了这些年和服务器斗智斗勇的这些配置,在不同情况下,需要设置的不一样。 对于从事操作系统开发而言,成千上万的软件包要构建和开发,那么服务器就是纯编译型服务器,那么这种情况下针对内存情况的配置如下。
如果内存吃紧,那么如下配置
echo 1 > /proc/sys/vm/swappiness echo 262144 > /proc/sys/vm/min_free_kbytes echo 40 > /proc/sys/vm/watermark_scale_factor echo 1 > /proc/sys/vm/overcommit_memory echo 629145600 > /proc/sys/vm/dirty_bytes echo 314572800 > /proc/sys/vm/dirty_background_bytes echo 500 > /proc/sys/vm/vfs_cache_pressure
值得注意的是
这样设置的情况下,编译任何工程都能工作
如果服务器内存本来就多,这个问题就变得简单了
1. 无需swap分区 2. 增大dirty_bytes 3. vfs_cache_pressure设置为1
当然,那不现实,写到这里,不禁发出感叹:
对于开头提到的问题解决效果呢,如下
systemtap是一个基于linux的性能诊断工具,能够对linux内核函数和linux应用的运行细节进行诊断的工具,最近遇到一个函数调用延迟比较大的问题,针对此问题,如果从庞大的代码中按照行分析,可能花费时间成本较大。我建议可以使用systemtap来定位进程的任意代码的运行耗时。这样就很轻松的定位性能问题了。下面就来介绍如何在arm64下使用systemtap定位程序的函数执行性能问题
apt install systemtap-sdt-dev libdw-dev
这里注意,默认系统的systemtap版本过旧,我们需要从systemtap官网下载,当前最新的是systemtap-5.3。
git clone git://sourceware.org/git/systemtap.git
如果sourceware速度慢,可以换清华源镜像站
wget https://mirrors.tuna.tsinghua.edu.cn/sourceware/systemtap/releases/systemtap-5.3.tar.gz
默认最新的代码是基于6.15-rc的内核,我们实际上内核使用的5.10。所以有一些代码需要稍微适配一下。
arm64内核从5之后都开启了vfs的namespace,如果不做额外修改,在arm64上,如下两个函数会报命名空间问题
kernel_read filp_open
报错信息如下
ERROR: modpost: module stap_ce5dbcb79b603543094c4f68fb16a1a8_943 uses symbol kernel_read from namespace VFS_internal_I_am_really_a_filesystem_and_am_NOT_a_driver, but does not import it. ERROR: modpost: module stap_ce5dbcb79b603543094c4f68fb16a1a8_943 uses symbol filp_open from namespace VFS_internal_I_am_really_a_filesystem_and_am_NOT_a_driver, but does not import it.
对于此问题,我们需要为systemtap在runtime的代码上声明一下命名空间,如下
# vim runtime/transport/symbols.c MODULE_IMPORT_NS(VFS_internal_I_am_really_a_filesystem_and_am_NOT_a_driver);
对于6的内核默认实现timer_delete_sync函数,但是我们还是在5.10的内核,我们使用的是del_timer_sync函数,所以需要针对就内核修改systemtap的代码,位置在runtime/transport/relay_compat.h
。
未修改代码如下
#ifdef STAPCONF_DEL_TIMER_SYNC #define STP_TIMER_DELETE_SYNC(a) del_timer_sync(a) #else #define STP_TIMER_DELETE_SYNC(a) timer_delete_sync(a) #endif
这里宏STAPCONF_DEL_TIMER_SYNC会决定具体的函数实现,我们为了代码修改最小化,如下修改
#ifndef STAPCONF_DEL_TIMER_SYNC #define STP_TIMER_DELETE_SYNC(a) del_timer_sync(a) #else #define STP_TIMER_DELETE_SYNC(a) timer_delete_sync(a) #endif
这里简单的修改了宏定义的作用范围
编译方法之前提过,如下
./configure make all -j8 make install -j8
systemtap代码量不算很大,可以之间在机器里面编译。这样make install就直接安装成功了
如果安装成功,那么我们可以看到如下信息
# stap --version Systemtap translator/driver (version 5.3/0.176, non-git sources) Copyright (C) 2005-2025 Red Hat, Inc. and others This is free software; see the source for copying conditions. tested kernel versions: 3.10 ... 6.15-rc enabled features: BPF LIBSQLITE3 LIBXML2 NLS JSON_C
我们需要使用systemtap,那么内核需要打开调试功能,整理如下
CONFIG_DEBUG_INFO=y CONFIG_KPROBES=y CONFIG_UPROBES=y CONFIG_RELAY=y CONFIG_DEBUG_FS=y CONFIG_MODULES=y CONFIG_TRACEPOINTS=y CONFIG_FUNCTION_TRACER=y
systemtap的原理是通过在系统中安插一个ko,通过此ko获取系统的详细信息,所以我们需要在内核中预置头文件。
手动预置的办法如下
make headers_install INSTALL_HDR_PATH=/tmp/kernel-header/ make firmware_install INSTALL_MOD_PATH=/tmp/kernel-header/ make modules_install INSTALL_MOD_PATH=/tmp/kernel-header/ cp --parents `find -type f -name "Makefile*" -o -name "Kconfig*"` /tmp/kernel-header/ cp Module.symvers /tmp/kernel-header/ cp System.map /tmp/kernel-header/ cp -rf scripts/ /tmp/kernel-header/ # arm bin cp -rf include/ /tmp/kernel-header/ cp -rf --parents arch/arm64/include /tmp/kernel-header cp -rf --parents arch/arm/include /tmp/kernel-header cp .config /tmp/kernel-header/ tar cvzf /tmp/kernel-header.tar.gz /tmp/kernel-header
此时我们将/tmp/kernel-header.tar.gz
解压为目录/usr/src/linux-headers-$(uname -r)
然后我们建立头文件链接如下
mkdir /lib/modules/${uname -r}/ ln -sf /usr/src/linux-headers-$(uname -r) build
此时,我们可以在机器中编译ko文件了
如果觉得上述手动预置不方便,那么可以自己从内核打包headers的deb,如下
make bindeb-pkg -j256
注意,上面是在已经构建过内核的情况下,这里只打包。如果没构建过内核,建议从头开始
make deb-pkg -j256
此时我们获得如下文件安装
dpkg -i linux-headers-5.10.198_5.10.198-69_arm64.deb linux-image-5.10.198_5.10.198-69_arm64.deb linux-image-5.10.198-dbg_5.10.198-69_arm64.deb linux-libc-dev_5.10.198-69_arm64.deb
为了能够获取应用程序的符号用于调试,我们需要安装对应应用程序的符号包,如下
# dpkg -i kylin-nm-dbgsym_3.20.1.7_arm64.ddeb
此时我们stap可以获取函数的符号如下
# stap -l 'process("/usr/bin/kylin-nm").function("*")' process("/usr/bin/kylin-nm").function("onShowControlCenter@frontend/tab-pages/lanpage.cpp:723") process("/usr/bin/kylin-nm").function("onSwithGsettingsChanged@frontend/tab-pages/lanpage.cpp:173") process("/usr/bin/kylin-nm").function("onUpdateConnection@frontend/tab-pages/lanpage.cpp:1150") process("/usr/bin/kylin-nm").function("onWiredEnabledChanged@frontend/tab-pages/lanpage.cpp:1233") ......
至此,我们可以开始调试了。
内核头文件完成之后,我们可以编写stap文件来进行调试。对于当前的需求是
所以代码如下:
# cat kylin.stp global entry_times probe process("/usr/bin/kylin-nm").function("LanPage::onWiredEnabledChanged") { entry_time = gettimeofday_us() entry_times[pid()] = entry_time } probe process("/usr/bin/kylin-nm").function("LanPage::onWiredEnabledChanged").return { if (entry_times[pid()] != 0) { exit_time = gettimeofday_us() elapsed = exit_time - entry_times[pid()] printf("[PID %d] [%s]: Took %ld us \n", pid(), "LanPage::onWiredEnabledChanged", elapsed) delete entry_times[pid()] } }
根据上面内容我们可以知道,这里我想定位/usr/bin/kylin-nm
的LanPage::onWiredEnabledChanged
函数的耗时。
我们如下方式运行
# stap -v ./kylin.stp Pass 1: parsed user script and 467 library scripts using 590948virt/103640res/5892shr/130664data kb, in 530usr/200sys/247real ms. Pass 2: analyzed script: 2 probes, 3 functions, 1 embed, 1 global using 598732virt/112916res/7120shr/138448data kb, in 330usr/10sys/339real ms. Pass 3: translated to C into "/tmp/stapbjEzxz/stap_44d2d295e641497a8b3e84b98ae25516_2499_src.c" using 598732virt/113108res/7312shr/138448data kb, in 10usr/240sys/252real ms. Pass 4: compiled C into "stap_44d2d295e641497a8b3e84b98ae25516_2499.ko" in 43320usr/8190sys/12830real ms. Pass 5: starting run.
可以看到有[PID 89425] [LanPage::onWiredEnabledChanged]: Took 174 us
的日志,这里可以看到按钮的响应时间是174us。
我们再点击关闭网络,如下
再点击打开网络,如下
反复10次,此时日志如下
[PID 89425] [LanPage::onWiredEnabledChanged]: Took 279 us [PID 89425] [LanPage::onWiredEnabledChanged]: Took 502 us [PID 89425] [LanPage::onWiredEnabledChanged]: Took 632 us [PID 89425] [LanPage::onWiredEnabledChanged]: Took 617 us [PID 89425] [LanPage::onWiredEnabledChanged]: Took 616 us [PID 89425] [LanPage::onWiredEnabledChanged]: Took 656 us [PID 89425] [LanPage::onWiredEnabledChanged]: Took 146 us [PID 89425] [LanPage::onWiredEnabledChanged]: Took 280 us [PID 89425] [LanPage::onWiredEnabledChanged]: Took 640 us [PID 89425] [LanPage::onWiredEnabledChanged]: Took 576 us [PID 89425] [LanPage::onWiredEnabledChanged]: Took 501 us [PID 89425] [LanPage::onWiredEnabledChanged]: Took 642 us [PID 89425] [LanPage::onWiredEnabledChanged]: Took 647 us [PID 89425] [LanPage::onWiredEnabledChanged]: Took 670 us [PID 89425] [LanPage::onWiredEnabledChanged]: Took 289 us [PID 89425] [LanPage::onWiredEnabledChanged]: Took 603 us [PID 89425] [LanPage::onWiredEnabledChanged]: Took 619 us [PID 89425] [LanPage::onWiredEnabledChanged]: Took 453 us [PID 89425] [LanPage::onWiredEnabledChanged]: Took 594 us [PID 89425] [LanPage::onWiredEnabledChanged]: Took 276 us
至此,我们可以监控任意的函数的执行时间。
如果需要定位其他的程序,就打上其他程序符号,找到需要定位的函数,修改kylin.stp即可
本文演示了在arm64上使用systemtap定位任意函数的耗时问题。systemtap还可以定位内核和其他问题。这里就不额外解释了。有兴趣可以自己研究,参与开源。
值得注意的是,如果systemtap在你的内核环境上运行不起来,你可能需要根据其分析兼容性问题。本文为了让systemtap-5.3在linux 5.10上运行,修改了两处代码的兼容问题。实际需要修复的内容通常是版本演变带来的问题,不会太困难。