本文分析基于Linux kernel 5.4
最近帮其他组的同事看了个有趣的问题,原因恰好和kernel log(又被称为kmsg/dmsg)中的速率限制有关。虽然知识点并不复杂,但鉴于自己寻找资料时的痛苦经历,还是决定将细节记录于此,以便后人查阅。
问题描述:子进程挂了以后,init
进程并没有打印出它的死亡信息。按理来说,子进程一旦挂掉,init
进程将会收到SIGCHLD信号(除非子进程中有线程呈现D状态)。在SIGCHLD的信号处理函数里,有如下代码,意味着无论如何都会打印log。然而在我们的案例中,这两句话都没有出现。
if (siginfo.si_code == CLD_EXITED) {
LOG(INFO) << name << " exited with status " << siginfo.si_status << wait_string;
} else {
LOG(INFO) << name << " received signal " << siginfo.si_status << wait_string;
}
我最初怀疑子进程并没有向init
发送SIGCHLD信号。但查阅之后的ps信息,发现该子进程消失的无影无踪。这也就意味着子进程被父进程调用waitpid
完美地回收了,否则它将呈现Zombie状态一直存活下去。这样看来,SIGCHLD已经发送,上述代码也被执行过了,可是log为什么凭空消失了呢?
根本的原因是kmsg对init
输出的log进行了速率限制,每5秒钟最多可以输出10行log,多出的部分将被丢弃。在我们的问题中,子进程挂掉之前init
进程的输出恰好达到了速率限制的上限,因此之后的输出都被丢弃了。以下是具体分析。
我们首先来看看init
进程中的log到底去向何处。
在进程起始阶段,通过InitKernelLogging
设置log的输出方式。
void InitKernelLogging(char** argv) {
SetFatalRebootTarget();
android::base::InitLogging(argv, &android::base::KernelLogger, InitAborter);
}
当有log输出时,最终会调用KernelLogLine
进行打印。
static void KernelLogLine(const char* msg, int length, android::base::LogSeverity severity,
const char* tag) {
// clang-format off
static constexpr int kLogSeverityToKernelLogLevel[] = {
[android::base::VERBOSE] = 7, // KERN_DEBUG (there is no verbose kernel log
// level)
[android::base::DEBUG] = 7, // KERN_DEBUG
[android::base::INFO] = 6, // KERN_INFO
[android::base::WARNING] = 4, // KERN_WARNING
[android::base::ERROR] = 3, // KERN_ERROR
[android::base::FATAL_WITHOUT_ABORT] = 2, // KERN_CRIT
[android::base::FATAL] = 2, // KERN_CRIT
};
// clang-format on
static_assert(arraysize(kLogSeverityToKernelLogLevel) == android::base::FATAL + 1,
"Mismatch in size of kLogSeverityToKernelLogLevel and values in LogSeverity");
static int klog_fd = OpenKmsg(); //static代码,只运行一次
if (klog_fd == -1) return;
int level = kLogSeverityToKernelLogLevel[severity];
// The kernel's printk buffer is only 1024 bytes.
// TODO: should we automatically break up long lines into multiple lines?
// Or we could log but with something like "..." at the end?
char buf[1024] __attribute__((__uninitialized__));
size_t size = snprintf(buf, sizeof(buf), "<%d>%s: %.*s\n", level, tag, length, msg);
if (size > sizeof(buf)) {
size = snprintf(buf, sizeof(buf), "<%d>%s: %zu-byte message too long for printk\n",
level, tag, size);
}
iovec iov[1];
iov[0].iov_base = buf;
iov[0].iov_len = size;
TEMP_FAILURE_RETRY(writev(klog_fd, iov, 1));
}
其中有两个关键的部分,一个是OpenKmsg
获取驱动文件的fd,另一个是writev
往文件中写入log。OpenKmsg
实质上就是打开/dev/kmsg
驱动文件。
#if defined(__linux__)
static int OpenKmsg() {
#if defined(__ANDROID__)
// pick up 'file w /dev/kmsg' environment from daemon's init rc file
const auto val = getenv("ANDROID_FILE__dev_kmsg");
if (val != nullptr) {
int fd;
if (android::base::ParseInt(val, &fd, 0)) {
auto flags = fcntl(fd, F_GETFL);
if ((flags != -1) && ((flags & O_ACCMODE) == O_WRONLY)) return fd;
}
}
#endif
return TEMP_FAILURE_RETRY(open("/dev/kmsg", O_WRONLY | O_CLOEXEC));
}
#endif
接着去了解kmsg驱动中的具体操作。当上层通过open打开驱动时,实际上调用的是devkmsg_open
。当上层通过writev
往驱动中写入数据时,实际上调用的是devkmsg_write
。
const struct file_operations kmsg_fops = {
.open = devkmsg_open,
.read = devkmsg_read,
.write_iter = devkmsg_write,
.llseek = devkmsg_llseek,
.poll = devkmsg_poll,
.release = devkmsg_release,
};
在devkmsg_open
中,变量user
需要重点关注。通过将其存入file->private_data,从而保证了两件事情。一是每个打开/dev/kmsg的进程都会有一个独立的devkmsg_user
对象,二是这个对象在驱动文件的生命周期内一直存在。
static int devkmsg_open(struct inode *inode, struct file *file)
{
struct devkmsg_user *user;
...
user = kmalloc(sizeof(struct devkmsg_user), GFP_KERNEL);
if (!user)
return -ENOMEM;
ratelimit_default_init(&user->rs);
...
file->private_data = user;
return 0;
}
user
中有一个rs对象,记录的正是速率限制的具体配置,它的初始化由ratelimit_default_init
完成。
struct devkmsg_user {
u64 seq;
u32 idx;
struct ratelimit_state rs;
struct mutex lock;
char buf[CONSOLE_EXT_LOG_MAX];
};
ratelimit_default_init
会将rs的interval
字段设置为默认的5HZ,将burst
字段设置为默认的10,刚好对应了我们上文中提到的每5秒钟最多可以输出10行log。
#define DEFAULT_RATELIMIT_INTERVAL (5 * HZ)
#define DEFAULT_RATELIMIT_BURST 10
static inline void ratelimit_default_init(struct ratelimit_state *rs)
{
return ratelimit_state_init(rs, DEFAULT_RATELIMIT_INTERVAL,
DEFAULT_RATELIMIT_BURST);
}
static inline void ratelimit_state_init(struct ratelimit_state *rs,
int interval, int burst)
{
memset(rs, 0, sizeof(*rs));
raw_spin_lock_init(&rs->lock);
rs->interval = interval;
rs->burst = burst;
}
到这里为止,我们介绍的还只是配置部分。那么速率限制的配置为何会生效呢?这就要接着去看writev
的写log动作了。
/* Keep both the 'on' and 'off' bits clear, i.e. ratelimit by default: */
#define DEVKMSG_LOG_MASK_DEFAULT 0
static unsigned int __read_mostly devkmsg_log = DEVKMSG_LOG_MASK_DEFAULT;
static ssize_t devkmsg_write(struct kiocb *iocb, struct iov_iter *from)
{
...
/* Ratelimit when not explicitly enabled. */
if (!(devkmsg_log & DEVKMSG_LOG_MASK_ON)) {
if (!___ratelimit(&user->rs, current->comm))
return ret;
}
...
具体输出的代码
}
devkmsg_log
默认为0,因此会进行__ratelimit
的判断。__ratelimit
中的代码只判断一件事:5秒内该函数是否被调用了超过10次。不过请注意,这个调用是和user
中的rs
对象绑定在一起的。也就是说,不同进程的统计分开计算,因为他们操作的rs
对象是独立的。因此,init
进程的速率限定只对本进程有效。
虽然devkmsg_log
默认为0,但是kernel开放了动态修改它的权限,以便在某些时刻关掉用户空间往/dev/kmsg写入log时的速率限制。这种动态修改的方式将在文末提及。
/*
* __ratelimit - rate limiting
* @rs: ratelimit_state data
* @func: name of calling function
*
* This enforces a rate limit: not more than @rs->burst callbacks
* in every @rs->interval
*
* RETURNS:
* 0 means callbacks will be suppressed.
* 1 means go ahead and do it.
*/
int ___ratelimit(struct ratelimit_state *rs, const char *func)
{
unsigned long flags;
int ret;
if (!rs->interval)
return 1;
/*
* If we contend on this state's lock then almost
* by definition we are too busy to print a message,
* in addition to the one that will be printed by
* the entity that is holding the lock already:
*/
if (!raw_spin_trylock_irqsave(&rs->lock, flags))
return 0;
if (!rs->begin)
rs->begin = jiffies;
if (time_is_before_jiffies(rs->begin + rs->interval)) {
if (rs->missed) {
if (!(rs->flags & RATELIMIT_MSG_ON_RELEASE)) {
printk_deferred(KERN_WARNING
"%s: %d callbacks suppressed\n",
func, rs->missed);
rs->missed = 0;
}
}
rs->begin = jiffies;
rs->printed = 0;
}
if (rs->burst && rs->burst > rs->printed) {
rs->printed++;
ret = 1;
} else {
rs->missed++;
ret = 0;
}
raw_spin_unlock_irqrestore(&rs->lock, flags);
return ret;
}
其实内核中很多其他模块也都采用了这种速率限定的打印模式,譬如binder。
Binder中有两个重要的文件,一个是binder.c
,另一个是binder_alloc.c
。它们分别在自己的文件头部进行了如下定义。pr_info_ratelimited
最终也是对printk做速率限制,而且其采用的配置同样是5秒10行。不过它里面的ratelimit_state
是局部静态变量,因此意味着速率限制的对象是每一句单独的打印。这一点和init
针对进程整体进行限制是完全不同的。
假设有多个进程同时调用binder.c
的binder_transaction
代码,而且函数内有两处调用pr_info_ratelimited
。那么在5秒内,每一处的pr_info_ratelimited
都受到10行的限制,这些调用可能来自不同的进程。而两处不同的pr_info_ratelimited
调用则互不干扰。
[printk_ratelimited]
/*
* ratelimited messages with local ratelimit_state,
* no local ratelimit_state used in the !PRINTK case
*/
#ifdef CONFIG_PRINTK
#define printk_ratelimited(fmt, ...) \
({ \
static DEFINE_RATELIMIT_STATE(_rs, \
DEFAULT_RATELIMIT_INTERVAL, \
DEFAULT_RATELIMIT_BURST); \
\
if (__ratelimit(&_rs)) \
printk(fmt, ##__VA_ARGS__); \
})
[binder.c]
#define binder_debug(mask, x...) \
do { \
if (binder_debug_mask & mask) \
pr_info_ratelimited(x); \
} while (0)
#define binder_user_error(x...) \
do { \
if (binder_debug_mask & BINDER_DEBUG_USER_ERROR) \
pr_info_ratelimited(x); \
if (binder_stop_on_user_error) \
binder_stop_on_user_error = 2; \
} while (0)
[binder_alloc.c]
#define binder_alloc_debug(mask, x...) \
do { \
if (binder_alloc_debug_mask & mask) \
pr_info_ratelimited(x); \
} while (0)
此外还有一个误解需要澄清。
网上的很多资料提到了两个sysctl节点:/proc/sys/kernel/printk_ratelimit_burst
和/proc/sys/kernel/printk_ratelimit
。
$ cat /proc/sys/kernel/printk_ratelimit_burst
10
$ cat /proc/sys/kernel/printk_ratelimit
5
看到这两个节点后,我们会本能地以为速率限制可以动态调节,直接往节点中echo一个新值不就可以了么。但真的会有效果么?答案是否定的。
{
.procname = "printk_ratelimit",
.data = &printk_ratelimit_state.interval,
.maxlen = sizeof(int),
.mode = 0644,
.proc_handler = proc_dointvec_jiffies,
},
{
.procname = "printk_ratelimit_burst",
.data = &printk_ratelimit_state.burst,
.maxlen = sizeof(int),
.mode = 0644,
.proc_handler = proc_dointvec,
},
原因是节点的值最终会反映到全局变量printk_ratelimit_state
中,而不论是init
进程,还是pr_info_ratelimited
,它们都会创建自己的ratelimit_state
对象,而不会使用这个全局的变量。因此printk_ratelimit_state
的修改并不会影响到init
进程和pr_info_ratelimited
的速率限制。
#define DEFINE_RATELIMIT_STATE(name, interval_init, burst_init) \
\
struct ratelimit_state name = \
RATELIMIT_STATE_INIT(name, interval_init, burst_init) \
DEFINE_RATELIMIT_STATE(printk_ratelimit_state, 5 * HZ, 10);
如果想要修改init
进程和pr_info_ratelimited
文件的速率限制,最直接的方式是改掉代码中以下两个变量的值,当DEFAULT_RATELIMIT_INTERVAL
修改为0时,意味着关闭速率限制。但这种方式需要重新编译,因此使用起来并不方便。
#define DEFAULT_RATELIMIT_INTERVAL (5 * HZ)
#define DEFAULT_RATELIMIT_BURST 10
如果仅仅想修改init
进程(用户空间)的速率限制,那么还可以使用Kernel开放的动态修改的权限。
这里有两种方式可以关闭速率限制,一种是修改kernel command line,另一种是修改sysctl的节点值。具体的原理较为简单,相信阅读完链接的代码大家都能弄懂。
1.修改kernel command line, 这种方式需要重启手机。以下是这个参数的相关介绍。
Add a "printk.devkmsg" kernel command line parameter which controls how
userspace writes into /dev/kmsg. It has three options:
* ratelimit - ratelimit logging from userspace.
* on - unlimited logging from userspace
* off - logging from userspace gets ignored
让手机进入fastboot模式可以在重启间隙修改这个值,如下操作。
fastboot --cmdline "其他参数 printk.devkmsg=on" boot boot.img
2.adb shell进入手机,然后执行"echo on > /proc/sys/kernel/printk_devkmsg"。这种方式可以在运行时动态设置,最为方便。
Furthermore, we can control the logging from a lower priority sysctl
interface - kernel.printk_devkmsg.
That interface will succeed only if printk.devkmsg *hasn't* been
supplied on the command line. If it has, then printk.devkmsg is a
one-time setting which remains for the duration of the system lifetime.
This "locking" of the setting is to prevent userspace from changing the
logging on us through sysctl(2).
通过sysctl设置的方式只在printk.devkmsg
的kernel command未设置时才有效,这一点在实际使用时需要注意。