Posts in Category: OperatingSystem

Linux 定时器的正确打开姿势

最近上层子系统使用我们封装的定时器时,发现定时不准确,比实时时间慢了一些。本文记录定位过程及解决方法。

使用定时器的一般步骤

Linux 下使用定时器的一般步骤如下:

(1) 使用 timer_create() 创建定时器

struct sigevent evp;

memset(&evp, 0, sizeof(struct sigevent));
evp.sigev_notify = SIGEV_SIGNAL;
evp.sigev_signo = timer_no;

if (timer_create(CLOCK_REALTIME, &evp, &tTimer[timer_no].timer) < 0)
{
return -1;
}

return 0;

其中的 timer_no 为定时器编号,tTimer 为事先定义的一个定时器结构体。刚创建的定时器不会自动运行。

(2) 使用 timer_settime() 设置定时器超时时间

timer_settime(tTimer[timer_no].timer, 0, &tTimer[timer_no].timevalue, NULL);

设置之后定时器立即开始运行。

(3) 在定时器线程中使用 sigwait() 等待定时器超时

sigset_t tsigmask;
int isigrcv, i;
long ret;

sigemptyset(&tsigmask);
for (i = 0; i < NUM_OF_TIMERS; i++)
{
sigaddset(&tsigmask, SIGRTMAX – i);
}

while (1)
{
ret = sigwait(&tsigmask, &isigrcv);
if (ret >= 0)
{
/* 调用定时器回调函数 */
}
}

查看内核的 date 是否准确

首先从源头开始,验证内核的实时时钟是否准确,借助 date 命令拷机实现。

先输入一次 date 命令,拷机一段时间之后,再输入一次 date 命令,通过对比两次命令输出的时间差与 SecureCRT 两条 log 记录的时间差,确认内核实时时钟准确无误。

(11:46:30.614) $ date
(11:46:30.645) Mon Jul 17 11:46:30 CST 2017
(13:48:02.798) $ date
(13:48:02.798) Mon Jul 17 13:48:02 CST 2017

查看用户态和内核的定时器计数是否一致

先在用户态的定时器线程的回调函数中,增加计数,定时器每超时一次就累加 1。

然后打开内核的 CONFIG_TIMER_STATS 配置项,重新编译内核并运行后,执行如下命令打开定时器统计:

echo 1 > /proc/timer_stats

之后使用如下命令查看所有定时器的计数信息等:

cat /proc/timer_stats

输出的信息类似如下:

Timer Stats Version: v0.2
Sample period: 55521.903 s

14205534, 1501 linux.out .common_timer_set (posix_timer_fn)

389290862 total events, 7011.482 events/sec

其中 linux.out 那行就是上层子系统所使用的定时器,超时计数为 14205534。而在应用代码中的计数为 14205535,比 timer_stats 的计数多了 1。且再次开启关闭一次定时器(同样通过调用 timer_settime() 实现),应用代码中的计数比 timer_stats 的计数多了 2,如此递增。此为疑点。

发现定时器使用时的问题

重新走查用户态中定时器相关代码,发现了问题所在。

在使用 timer_create() 创建定时器之后,设置 timer_settime() 时,对于第三个入参 new_value,只将超时时间赋值给了 it_valueit_interval 设置为 0。

参考 timer_settime(2) 中关于 it_valueit_interval 的说明:

If new_value->it_value specifies a nonzero value (i.e., either subfield is nonzero), then timer_settime() arms (starts) the timer, setting it to initially expire at the given time. (If the timer was already armed, then the previous settings are overwritten.) If new_value->it_value specifies a zero value (i.e., both subfields are zero), then the timer is disarmed.

The new_value->it_interval field specifies the period of the timer, in seconds and nanoseconds. If this field is nonzero, then each time that an armed timer expires, the timer is reloaded from the value specified in new_value->it_interval. If new_value->it_interval specifies a zero value then the timer expires just once, at the time specified by it_value.

也就是说,it_value 确定第一次超时时间,it_interval 确定后续的超时时间。那么现有的代码中如何实现定时器多次执行的呢?

原来是在定时器线程的 while 循环部分再次调用 timer_settime() 重新设置一次 it_value

while (1)
{
ret = sigwait(&tsigmask, &isigrcv);
if (ret >= 0)
{
/* 调用定时器回调函数 */
tTimer[timer_no].clkCallback(tTimer[timer_no].argCall);
timer_settime(tTimer[timer_no].timer, 0, &tTimer[timer_no].timevalue, NULL);
}
}

那么在定时器此次超时到再次调用 timer_settime() 启动定时器之间,存在一定的延迟而引入误差,长时间运行之后此误差将累积,导致定时器比实时时间慢。

这也可解释为什么启动停止定时器会导致用户态的计数比内核的计数多 1 的现象。内核的定时器已经超时停止,但用户态回调最后还会累加 1。

解决方法

  • 在调用 timer_settime() 时,同时设置 it_valueit_interval 的值,使定时器自动重新加载并循环运行。
  • 在定时器回调函数中,去掉调用 timer_settime() 重新设置定时器超时时间的代码。仅保留调用上层子系统挂载的回调函数即可。

这才是 Linux 定时器的正确打开姿势。

以上。

mmap 失败并返回 -EPERM 错误问题

问题现象

在我们的应用系统初始化过程中,用户态程序会读取 /proc/task_info 第一个字段的值作为物理地址传入 mmap/dev/mem 做内存映射。/proc/task_info 的内容如下:

$ cat /proc/task_info
0x81b0000 0xd555555555555555 0x5

也就是将 0x81b0000 (每次运行值可能不同)作为物理地址传入 mmap,此时返回 errno 为 -1,表示无权限:

#define EPERM 1 /* Operation not permitted */

源码分析

内存设备节点 /dev/mem 作为可随机读写的字符设备,在内核源码的 drivers/char/mem.c 中初始化(/dev/kmem, /dev/null, /dev/zero, /dev/random, /dev/urandom 等设备节点也都在这个文件中定义),对 /dev/memmmap 操作定义为 mmap_mem()。在 mmap_mem() 中返回 -EPERM 的代码片段如下:

if (!range_is_allowed(vma->vm_pgoff, size))
return -EPERM;

当定义了 CONFIG_STRICT_DEVMEM 时,在 range_is_allowed() 中会逐页调用 devmem_is_allowed() 检查是否可访问:

static inline int range_is_allowed(unsigned long pfn, unsigned long size)
{
u64 from = ((u64)pfn) << PAGE_SHIFT;
u64 to = from + size;
u64 cursor = from;

while (cursor < to) {
if (!devmem_is_allowed(pfn)) {
printk(KERN_INFO
“Program %s tried to access /dev/mem between %Lx->%Lx.\n”,
current->comm, from, to);
return 0;
}
cursor += PAGE_SIZE;
pfn++;
}
return 1;
}

devmem_is_allowed() 由各个体系结构实现,以 PowerPC 为例,在 arch/powerpc/mm/mem.c 中,同样也是定义了 CONFIG_STRICT_DEVMEM 才有实现:

int devmem_is_allowed(unsigned long pfn)
{
if (iomem_is_exclusive(pfn << PAGE_SHIFT))
return 0;
if (!page_is_ram(pfn))
return 1;
if (page_is_rtas_user_buf(pfn))
return 1;
return 0;
}

devmem_is_allowed() 返回 1 表示允许访问,0 表示不允许访问。其中第一个 if 语句调用的 iomem_is_exclusive() 是用于判断 PCI 的内存空间(PCI mem)是否互斥访问的。由于我们访问的是内核生成的 /proc/task_info,与 PCI 无关,因此 mmap 失败是在 devmem_is_allowed() 最后的 return 0 返回了不允许访问。

解决方法

方法一:修改 devmem_is_allowed() 函数

devmem_is_allowed() 函数中,当访问的是 /proc/task_info 对应的页号时,返回 1 表示允许访问。

这种方法实现复杂,需要在生成 /proc/task_info 时保存其对应的页号(在 fs/proc/taskinfo.c 文件的 task_info_init() 函数中分配的页号);同时这个方法污染了内核代码,不具有通用性。

方法二:关闭 CONFIG_STRICT_DEVMEM 配置

从前面的说明中可以看出,几个检查函数都是在定义了 CONFIG_STRICT_DEVMEM 才生效的,因此最简单的方法,就是将内核配置中 CONFIG_STRICT_DEVMEM 选项关闭并重新编译内核,这也是我们的应用系统所采用的方法。

以上。

Windows 保存的共享目录认证信息如何删除?

调试 Samba 服务的用户登录时,需要在 Windows 下尝试使用不同的用户名和密码登录 Samba 共享目录。若在登录时选择了「记住密码」,要使用其它用户名登录时,就需要删除之前保存的认证信息。本文参考 ServerFault,在 Windows XP 和 Windows 7 分别验证并记录。

Windows XP

进入控制面板,双击「用户账户」,选择「高级」选项卡,再点击「管理密码」。在弹出的「存储用户名和密码」窗口中,选择对应 Samba 服务器 IP 的一项删除即可。

管理用户名和密码

Windows 7

进入控制面板,双击「管理工具」,「服务」,找到「Workstation」,右键选择「重新启动」即可。

以上。