随着人员逐渐多,并且处理的事情越来越多,导致服务经常出现io占用高,cpu占用高,内存占用高三种情况。本文通过crontab的方式定时进行查询和设置,来实现定期对服务的资源限制,从而确保服务不会突然卡死的情况出现
在一个服务器上,多人进行编辑,编译时,通常情况下,编译的时候会把cpu,memery,io占满,三者只要有其一出现满负载,则打开的vim程序会出现,卡顿,杀掉,卡死。
对于cpu满负载而言,通常是vim程序得不到该有的调度,主要原因是vim的PRI和NI是80和0,而常规程序的优先级也是80和0,此时vim程序卡顿
对于memory满负载而言,通常是系统内存不足时,不得已情况下,通过out_of_memory函数选择一个oom score最低的程序进行杀掉,vim通常会是得分较低的进程。主要原因是vim的score是0,而常规程序的score也是0。此时vim可能被杀死
对于io负载而言,通常是io状态是陷入并且阻塞的,如果io负载高,则短时间内所有进程无法都被block住,无法被调度,此时vim出现卡死
所以,为了解决编译和编辑时的服务器负载问题,需要通过一个脚本来实现对cpu,io,memory的控制,从而使得编辑程序vim得到该有的调度,而编译程序也能正常运行。
为了实现上述的功能,需要借助cpulimit,cgroup, ionice三个工具
cpulimit是一个限制cpu使用率的工具,我们可以通过-p参数指定进程的pid,通过-l参数设置使用率百分比,从而限制进程使用率。介绍如下:
-p, --pid=N pid of the process -l, --limit=N percentage of cpu allowed from 1 up. Usually 1 - 4800, but can be higher on multi-core CPUs (mandatory)
所以,对于cpu满负载的情况,通过cpulimit限制当前最高使用率的进程,从而对突出的程序进行有效控制。让系统有办法调度到其他的程序
cgroup是用作资源管理的内核框架,它作为文件系统存放于linux系统中,我们可以通过cgroup设置某个进程的内存使用情况,具体如下:
/sys/fs/cgroup/memory/kylin_limit/memory.limit_in_bytes
所以,对于io满负载的情况,通过cgroups设置memory.limit_in_bytes来限制高占用进程的内存占用率,从而对高内存占用程序进行有效控制,让系统其他进程能够正常申请到内存。
不过值得注意的是,对于进程的内存使用限制可能导致进程运行失败,所以通常我们在服务器上设置单个进程使用不超过总内存的10%,通常服务器的总内存为64G,则单个进程使用内存不应该超过6.4G。通常来说,单个进程的内存使用量如果超过6.4G已经存在异常。所以没有问题。
但是如果是小内存的设备,通过memory.limit_in_bytes来限制指定进程的内存使用率并不可取,假设在内存4G的机器上,我们限制进程的内存使用率仍是10%,则单个进程使用内存不超过400M,又由于很多程序可能使用内存大于400M(属于正常现象),这会导致进程的oom,所以我们应该做的是通过增大swap来将内存压力转换为io压力。
通过swap来转换内存压力为io压力的方法如下:
mkswap /dev/sda1 swapon /dev/sda1
如果我们不是真实的设备,可以将swap开启在img文件上,如下
dd if=/dev/zero of=/tmp/swap bs=512 count=20480000 mkswap /tmp/swap swapon /tmp/swap
ionice是用作调整进程的io优先级的命令,通过ionice命令,可以设置指定进程作为实时的io调度,从而加速系统对某些进程的io行为。主要如下:
ionice -c 2 -n 7 -p ${pid} # 设置pid进程的调度为尽力,并优先级设置为7 ionice -c 1 -p ${pid} # 设置pid进程的调度为实时
这里-c可以设置优先级,-n设置优先级,-p设置pid,介绍如下
用法: ionice [选项] -p <pid>... ionice [选项] -P <pgid>... ionice [选项] -u <uid>... ionice [选项] <命令> 设置或更改进程的 IO 调度类别和优先级。 选项: -c, --class <类别> 调度类别的名称或数值 0: 无, 1: 实时, 2: 尽力, 3: 空闲 -n, --classdata <数字> 指定调度类别的优先级(0..7),只针对 “实时”和“尽力”类别 -p, --pid <pid>... 对这些已运行的进程操作 -P, --pgid <pgrp>... 对这些组中已运行的进程操作 -t, --ignore 忽略失败 -u, --uid <uid>... 对属于这些用户的已运行进程操作 -h, --help 显示此帮助 -V, --version 显示版本
所以,对于满io负载的情况,可以通过ionice命令对io刷盘进程如flush进行实时调度,对io使用率高的程序使用优先级最低为7的尽力调度,从而可以使得vim的写操作可以正常的flush,并且可能从普通进程上抢到io的调度。
根据上述的介绍,我们可以通过三个工具完成对应的功能,现在介绍如何实施
为了限制高占用的进程,我们需要在系统长时间占用高的情况下找到占用高的进程,因为某些短时进程会占用很高(例如top后一直按enter来一直刷新),这些我们应该忽略。所以需要先获取平均负载,如下:
load=`cat /proc/loadavg | cut -f1 -d' '`
上面可以获取1分钟内的平均负载,如果平均负载大于100,则认为1分钟内,进程占用高,可能在后台执行编译动作
declare -i loadavg=`echo "${load} * 100" | bc | awk -F. '{print $1}'` if (( ${loadavg} >= ${LOAD_CPU_THRESHOLD} ))
此时我们可以通过ps命令找出最高占用的那个进程
high_exe=`ps -eo user,pid,pcpu,pmem,args --sort=-pcpu |sed -n 2p | xargs`
根据ps的结果,可以获取进程的pid和name,如下
pid=`echo ${high_exe} | awk '{print $2}'` pid_name=`echo ${high_exe} | awk '{print $5}'`
根据pid值,我们可以知道是谁连接的ssh客户端,找到ip如下
who=`cat /proc/${pid}/environ | tr '\0' '\n' | grep SSH_CONNECTION | cut -d= -f2 | awk '{print $1":" $2}'`
同时通过cpulimit设置此进程的占用率不超过100%
cpulimit -p ${pid} -l 100 > /dev/null 2>&1 &
至此,如果crontab每一10分钟轮询一次,系统将查询最近1分钟内是否负载高,如果负载高,则找出当前占用最高的进程,设置其占用率不超过100%
为了限制高内存占用的程序,我们需要在系统剩余内存不足时找出内存占用最高的进程,然后将其设置为总内存的10%,从而让其他程序有可能申请到内存。
基于此思路,我们需要判断当前内存剩余量,如下
declare -i free=`free -m | sed -n 2p | xargs | cut -f7 -d' '`
上面可以获取到剩余内存,当然,同样通过free命令,我们也可以获取到总共内存,我们需要判断一下剩余内存是否小于总共内存的比例即可,具体比例可以自行设置,建议10%
如果剩余内存小于总共内存的10%,我们需要找到高内存的进程,如下
high_mem_exe=`ps -eo pid,pcpu,pmem,args --sort=-pmem |sed -n 2p | xargs`
同样的,通过上面可以知道高内存占用进程的pid和name,以及谁ssh登录的,如下
pid=`echo ${high_mem_exe} | awk '{print $1}'` pid_name=`echo ${high_mem_exe} | awk '{print $4}'` who=`cat /proc/${pid}/environ | tr '\0' '\n' | grep SSH_CONNECTION | cut -d= -f2 | awk '{print $1":" $2}'`
我们知道pid之后,便可以通过cgroup设置进程的最大内存使用量
[ ! -d /sys/fs/cgroup/memory/kylin_limit ] && mkdir /sys/fs/cgroup/memory/kylin_limit echo ${pid} > /sys/fs/cgroup/memory/kylin_limit/tasks echo $(expr $total \/ 10 \* 1024) > /sys/fs/cgroup/memory/kylin_limit/memory.limit_in_bytes
这里total是总内存大小,可以通过free命令获取,我们可以设置cgroup下的kylin_limit组的内存使用率不能超过总内存的10%
至此,如果crontab每10分钟轮询一次,系统将查询当前剩余内存,如果剩余内存不足总内存的某个比例,则可以通过cgroups设置最高内存占用率的进程其最大内存使用量不超过总内存的10%
程序的io都是block的,因为谁也不知道陷入io处理需要多久时间,所以通常如果io存在瓶颈,大部分情况下都是cpu在等io,从而导致vim卡死的情况
通常来说,对于磁盘来说,刷盘是由内核线程[kworker/uxx:x+flush-x:xxx]
实现,如果是文件系统write,也可能是mount进程中实现。本文只讨论常出现的flush
首先我们需要找到io占用率高的进程,如下
io_message=`iotop -n 1 -b -o | grep -v WRITE | xargs`
从上述的信息获取到io占用率和pid以及name
percent=`echo ${io_message} | awk '{print $10}'` name=`echo ${io_message} | awk '{print $12}'` pid=`echo ${io_message} | awk '{print $1}'`
如果进程是flush,则当前io压力最大的是刷盘线程
if [[ ${pid_name} =~ "kworker" ]] then if [[ ${pid_name} =~ "flush" ]] then
此时我们可以通过ionice命令设置flush线程为实时io调度,如下
ionice -c 1 -p ${pid}
如果io占用率超过99%了,则说明有io占用很高的进程,如下
echo ${percent} | egrep "^99.**" > /dev/null
此时我们可以将此进程设置为尽力,优先级放最低为7,此时程序能够正常跑即可,相当于以时间换内存空间
ionice -c 2 -n 7 -p ${pid}
至此,如果crontab每10分钟轮询一次,系统将找到io占用高的进程,先判断是否为flush,如果是flush则调整为实时,如果不是,则是其他进程io占用高,此时将其他进程调整为尽力,优先级最低为7
当然,如果io密集型的任务多,我们可以让其1分钟运行一次,在不改变crontab的前提下,可以做个循环如下:
for((i=0;i<10;i++)); do io_policy ret=$? if [ "${ret}"x == "1" ] then sleep 60 fi done
对于上述的策略,我们可以10分钟运行一次,首先将其脚本命名为kylin-server-status.sh,通过crontab -e编写如下:
*/10 * * * * /usr/local/bin/kylin-server-status.sh
至此,服务器会每10分钟轮询一次
对于系统,我们可以针对一个库的函数调用进行ld preload来hack,这样我们只要知道核心库的函数名和相关信息,就能通过hook的方式来伪造系统核心信息
为了跟踪函数的调用,可以通过ltrace,如下:
ltrace -p `pidof activation-daemon`
这样我们可以找到关键函数:license_should_escape
我们知道了关键函数,则可以编写一个简单的c如下:
#include <syslog.h> int license_should_escape() { syslog(LOG_ERR, "%s kylin hacked\n", __func__); return 1; }
此时我们通过gcc可以将其编译成动态库so,如下:
gcc kylin_hack.c -o libkylin_hack.so --shared
此时我们生成了库 libkylin_hack.so
。接下来需要进行ld preload如下:
echo /usr/lib/aarch64-linux-gnu/libkylin_hack.so >> /etc/ld.so.preload
进行如上的命令之后,我们可以直接重启
此时我们发送dbus,可以看到激活已经被破解,如下:
root@kylin:~# dbus-send --system --print-reply --dest=org.freedesktop.activation /org/freedesktop/activation org.freedesktop.activation.interface.date method return time=1723801648.866529 sender=:1.124 -> destination=:1.125 serial=9 reply_serial=2 string "2099-12-31" int32 0
我们查看设置界面的信息,可以看到已激活
我们知道linux都是基于dbus来进行通信的,dbus分为server和client,也就是说,如果应用程序作为client发送消息,我们只需要劫持这条dbus即可将错误的dbus信息返回给client,这样就能修改核心信息,
对于我们感兴趣的调用,我们可以通过监听的方式找到调用,如下:
dbus-monitor --system "destination=org.freedesktop.activation"
此时我们可以抓到某个method call如下:
method call time=1723771942.107726 sender=:1.4302 -> destination=org.freedesktop.activation serial=26 path=/org/freedesktop/activation; interface=org.freedesktop.activation.interface; member=date
我们编写pro如activation-daemon.pro文件如下:
QT += dbus TARGET = activation-daemon SOURCES += activation-daemon.cpp HEADERS += activation-daemon.h 然后编写dbus interface class为activation-daemon.h如下: #include <QCoreApplication> #include <QtDBus> // https://doc.qt.io/qt-5/qdbusabstractadaptor.html class ActivationDaemon : public QDBusAbstractAdaptor { Q_OBJECT Q_CLASSINFO("D-Bus Interface", "org.freedesktop.activation.interface") public: ActivationDaemon(QObject *parent = nullptr) : QDBusAbstractAdaptor(parent) {} public slots: QString date() { return "kylin hack"; } };
最后实施这个类activation-daemon.cpp 如下:
#include "activation-daemon.h" int main(int argc, char *argv[]) { QObject object; QCoreApplication app(argc, argv); QDBusConnection connection = QDBusConnection::systemBus(); ActivationDaemon ac(&object); connection.registerObject("/org/freedesktop/activation", &object); connection.registerService("org.freedesktop.activation"); qDebug() << "Kylin Hacking..."; return app.exec(); }
编译后生成如下:
qmake && make
此时我们生成二进制为activation-daemon
,将其替换到系统如下:
mv activation-daemon /usr/sbin/activation-daemon
此时我们通过dbus-send验证这条dbus即可,如下
root@kylin:~# dbus-send --system --print-reply --dest=org.freedesktop.activation /org/freedesktop/activation org.freedesktop.activation.interface.date method return time=1723772343.990579 sender=:1.4205 -> destination=:1.4309 serial=115 reply_serial=2 string "kylin hack"
可以看到这条dbus的响应已经变成我们自己写的dbus服务了。大功告成
针对上述这种手段,我们可以知道,基于篡改二进制导致的劫持可以导致dbus的响应被篡改,为了保障核心相关二进制不被篡改,我们可以通过加密解密的方式完成,详情请查看如下:
激活防破解方案-使用非对称加密和数字签名
根据之前的讨论,我们CD盘的短期目标是基于overlayfs来实现系统数据和应用数据的隔离(形式上)。长期目标是将应用数据和系统数据完全隔离开来。本文主要讨论peony上显示应用盘的基本改造流程
根据上图我们可以知道,对于overlayfs,区分lower和upper两层。我们可以将lower设置为sysroot,将upper设置为appfs。这样,我们发行操作系统的时候,默认情况下lower是发行的原始操作系统,而upper是除了原始发行的操作系统之外的所有改动。
我们将发行操作系统之外的所有改动都认为是应用的改动,所以直接认为upper就是应用盘。
对于peony,我们需要修改sidebar上的显示,显示系统盘和应用盘。效果如下:
这里我们文件系统盘是默认的overlay,如下:
kylinoverlay 19G 8.3G 9.5G 47% /
这里应用盘是appfs目录,如下:
/dev/mmcblk0p5 19G 8.3G 9.5G 47% /appfs
这里系统盘是sysroot目录,如下:
root@kylin:~# ls /sysroot/ bin boot data dev etc home lib media mnt opt proc root run sbin srv sys tmp usr var work
对于代码,主要如下:
From 79f4bfabfde8d8d2deaaf2908933c1be36a1d198 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=94=90=E5=B3=B0?= <tangfeng@kylinos.cn> Date: Wed, 15 Jan 2025 17:36:13 +0800 Subject: [PATCH] changelog: 3.14.4.5-0k3.0tablet18rk3.egf1.3 --- .../model/side-bar-file-system-item.cpp | 22 ++++++++++++++----- translations/libpeony-qt/libpeony-qt_zh_CN.ts | 4 ++++ 3 files changed, 31 insertions(+), 5 deletions(-) diff --git a/libpeony-qt/model/side-bar-file-system-item.cpp b/libpeony-qt/model/side-bar-file-system-item.cpp index 6268ddfb6..0b6664966 100644 --- a/libpeony-qt/model/side-bar-file-system-item.cpp +++ b/libpeony-qt/model/side-bar-file-system-item.cpp @@ -169,6 +169,11 @@ void SideBarFileSystemItem::initDirInfo(const QString &uri) m_displayName = tr("Data"); m_iconName = "drive-harddisk"; } + + if (uri == "file:///appfs") { + m_displayName = tr("Appfs"); + m_iconName = "drive-harddisk"; + } } void SideBarFileSystemItem::initComputerInfo() @@ -259,6 +264,7 @@ void SideBarFileSystemItem::initVolumeInfo(const Experimental_Peony::Volume &vol }else{ m_uri = "file://" + m_uri; } + /* 文件系统项特殊处理 */ if("file:///" == m_uri){ m_unmountable = m_mountable = m_ejectable = m_stopable = false; @@ -270,11 +276,11 @@ void SideBarFileSystemItem::initVolumeInfo(const Experimental_Peony::Volume &vol m_mounted = true; m_displayName = QObject::tr("Data"); m_iconName = "drive-harddisk"; - } - - /* 隐藏指定的挂载点 */ - if(m_device == getDeviceMount("/media/root-rw") || m_device == getDeviceMount("/media/root-ro")){ - m_hidden = true; + }else if("file:///appfs" == m_uri){ + m_unmountable = m_mountable = m_ejectable = m_stopable = false; + m_mounted = true; + m_displayName = QObject::tr("Appfs"); + m_iconName = "drive-harddisk"; } // kydrive特殊处理 @@ -612,6 +618,12 @@ void SideBarFileSystemItem::findChildren() m_model->endInsertRows(); } + if (FileUtils::isFileDirectory("file:///appfs")) { + m_model->beginInsertRows(this->firstColumnIndex(), m_children->count(), m_children->count()); + SideBarFileSystemItem* item = new SideBarFileSystemItem("file:///appfs", nullptr, this, m_model); + m_children->append(item); + m_model->endInsertRows(); + } }else{ //对挂载点进行已存在文件的枚举操作 QString enumdir = m_uri; diff --git a/translations/libpeony-qt/libpeony-qt_zh_CN.ts b/translations/libpeony-qt/libpeony-qt_zh_CN.ts index d8de5ad92..68bdf8f4b 100644 --- a/translations/libpeony-qt/libpeony-qt_zh_CN.ts +++ b/translations/libpeony-qt/libpeony-qt_zh_CN.ts @@ -3792,6 +3792,10 @@ Do you want to delete the link file?</source> <source>Data</source> <translation>数据盘</translation> </message> + <message> + <source>Appfs</source> + <translation>应用盘</translation> + </message> </context> <context> <name>Peony::SideBarMenu</name> -- 2.25.1
这里完成了应用盘的新增工作,识别/appfs的目录,将其作为单独的硬盘显示在peony的侧边栏
diff --git a/libpeony-qt/model/side-bar-file-system-item.cpp b/libpeony-qt/model/side-bar-file-system-item.cpp index 0b6664966..f38d678e7 100644 --- a/libpeony-qt/model/side-bar-file-system-item.cpp +++ b/libpeony-qt/model/side-bar-file-system-item.cpp @@ -174,6 +174,11 @@ void SideBarFileSystemItem::initDirInfo(const QString &uri) m_displayName = tr("Appfs"); m_iconName = "drive-harddisk"; } + + if (uri == "file:///sysroot") { + m_displayName = tr("Sysroot"); + m_iconName = "drive-harddisk"; + } } void SideBarFileSystemItem::initComputerInfo() @@ -281,6 +286,11 @@ void SideBarFileSystemItem::initVolumeInfo(const Experimental_Peony::Volume &vol m_mounted = true; m_displayName = QObject::tr("Appfs"); m_iconName = "drive-harddisk"; + }else if("file:///sysroot" == m_uri){ + m_unmountable = m_mountable = m_ejectable = m_stopable = false; + m_mounted = true; + m_displayName = QObject::tr("Sysroot"); + m_iconName = "drive-harddisk"; } // kydrive特殊处理 @@ -618,6 +628,13 @@ void SideBarFileSystemItem::findChildren() m_model->endInsertRows(); } + if (FileUtils::isFileDirectory("file:///sysroot")) { + m_model->beginInsertRows(this->firstColumnIndex(), m_children->count(), m_children->count()); + SideBarFileSystemItem* item = new SideBarFileSystemItem("file:///sysroot", nullptr, this, m_model); + m_children->append(item); + m_model->endInsertRows(); + } + if (FileUtils::isFileDirectory("file:///appfs")) { m_model->beginInsertRows(this->firstColumnIndex(), m_children->count(), m_children->count()); SideBarFileSystemItem* item = new SideBarFileSystemItem("file:///appfs", nullptr, this, m_model); diff --git a/translations/libpeony-qt/libpeony-qt_zh_CN.ts b/translations/libpeony-qt/libpeony-qt_zh_CN.ts index 68bdf8f4b..3c0814fc8 100644 --- a/translations/libpeony-qt/libpeony-qt_zh_CN.ts +++ b/translations/libpeony-qt/libpeony-qt_zh_CN.ts @@ -3792,6 +3792,10 @@ Do you want to delete the link file?</source> <source>Data</source> <translation>数据盘</translation> </message> + <message> + <source>Sysroot</source> + <translation>系统盘</translation> + </message> <message> <source>Appfs</source> <translation>应用盘</translation>
这里新增了系统盘的显示,指向了sysroot目录。
通过上述操作,我们可以在文件管理器中正常的导航到/appfs和/sysroot两个目录。这两个目录实际上就是overlay的lower和upper。至此,在当前阶段,系统的文件存放在sysroot,而应用的文件存放在appfs。我们完成了应用程序和操作系统的分离。
我们在看内核代码的时候,有一个关于C语言的一个小技巧,个人觉得可以介绍分享一下,做个记录。从而方便大家看代码的时候心里直接有个答案,无需脑子里面再转个弯。
我们知道内核头文件会定义结构体,在定义结构体的时候,默认会把重要的结构体的第一项作为子结构体。如下示例:
struct __drm_planes_state { struct drm_plane *ptr; struct drm_plane_state *state, *old_state, *new_state; };
这里我们有个C语言的知识点如下:
struct drm_plane *ptr 的地址是struct __drm_planes_state的地址
也就是说,如果我们一直在操作ptr指针,其实我可以随时和任意的操作struct __drm_planes_state指针,伪代码如下:
struct __drm_planes_state* stat = (struct __drm_planes_state*) &ptr; if(stat->state){ ...... }
这里必须要清楚的是,ptr指针一定要是__drm_planes_state的成员,不能是从其他地方构造和赋值的指针值地址,错误的例子如下:
struct drm_plane *p1 = ptr; stat = (struct __drm_planes_state*) &p1;
这里p1我们不能直接去做取&运算,因为它本身不是__drm_planes_state的成员, 它只是普普通通的一个栈区地址。
#include <stdio.h> #include <stdlib.h> struct lower { int a; }; struct upper { struct lower *k; int b; }; int main() { struct upper *u = malloc(sizeof(struct upper)); struct lower *l = u->k; u->b = 2; struct upper* t1 = (struct upper*)&(u->k); struct upper* t2 = (struct upper*)&l; printf("t1=%p t2=%p t1->b=%d \n", t1, t2, t1->b); }
这里我们构造了一个upper的结构体,设置成员b的值为2,然后我们提取了t1和t2,并打印了地址,得到输出如下:
# gcc test.c -o test && ./test t1=0x558beacb70 t2=0x7ffd087628 t1->b=2
发现没,t1大概在堆区地址范围上,t2在栈区范围上,我们通过t1能够直接找到b,其值为2。
这里我们知道了一个内核通用技巧,我们可以在内核代码中经常看到直接强制类型转换就拿到了父的结构体的指针,然后直接操作代码。非常方便大家理解内核的逻辑。
以后大家看内核代码的时候,这类操作就不需要停顿下来脑子去转弯了。
如果经常看代码,我们可以发现内核充斥着大量的container_of函数,这个函数的意思是:
输入一个成员变量实体,一个父结构体类型,一个结构体成员变量声明,输出结构体父指针
为什么会有这样的函数呢,我们可以根据上文我们可以很容易提出疑问
如果我想找到父结构体指针,那么我的成员必定是第一个成员,那多不方便啊。如果这个成员不是第一个,那有啥好办法能够找到父结构体指针呢? 关于此,我们有两个知识点需要准备:
那基于此,很容易得出这么一个想法
我知道结构体的成员地址,然后推算我之前有多少的偏移,直接拿自己的指针减去这个偏移不就是父指针的地址了么 所以,我们开始解析container_of的宏定义,如下:
#define container_of(ptr, type, member) ({ \ void *__mptr = (void *)(ptr); \ ((type *)(__mptr - offsetof(type, member))); })
我们还需要看offsetof的定义,根据内核头文件,我们可以查到:
#define offsetof(TYPE, MEMBER) ((size_t)&((TYPE *)0)->MEMBER)
这里有一个技巧,那就是
运用0地址做强制类型转换,然后去取类型的成员,这样就能拿到成员在结构体的偏移量,然后将其强制类型转换成size_t类型用作指针的减法运算
#include <stdio.h> #include <stdlib.h> #include <stddef.h> #ifndef offsetof #define offsetof(TYPE, MEMBER) ((size_t)&((TYPE *)0)->MEMBER) #endif #define container_of(ptr, type, member) ({ \ void *__mptr = (void *)(ptr); \ ((type *)(__mptr - offsetof(type, member))); }) struct lower { int a; }; struct upper { int c,d,e,f; struct lower *k; int b; }; int main() { struct upper *u = malloc(sizeof(struct upper)); // struct upper* t = ({ void *__mptr = (void *)(&u->k); ((struct upper *)(__mptr - ((size_t)&((struct upper *)0)->k))); }); struct upper* t = container_of(&u->k, struct upper, k); printf("t=%p up=%p k=%p\n", t, u, &u->k); }
这里我们传入u->k的地址,upper的结构体定义,k成员,我们就能拿到upper的指针t
此时我们运行如下:
# gcc test.c -o test && ./test t=0x559829eb70 up=0x559829eb70 k=0x559829eb80
可以发现t,up的地址完全相等,k刚刚差一个offset地址偏移,也就是4*4=16,就是int c,d,e,f;的占用空间
这里我们知道了内核非常普遍的container_of的实现,它能够直接获取成员的父结构体指针
至此,我们知道了内核操作结构体的小技巧,通过这个技巧,我们可以轻松的找到结构体指针以及成员。这里作为内核的入门知识,对了解内核的人而言至关重要。