概述
复杂的嵌入式系统中,常常同时运行着相当多的进程。这些进程之间频繁的进行着大量的通信动作。进程的运行状态与这些不断发生的通信有着直接和紧密的联系。通过对进程间通信的监视,开发人员可以掌控系统内部运转的状态。发现错误时,利用获取到的进程间通信的信息,调试工程师更容易发现问题之所在。
但是,嵌入式系统与开发人员的接口往往较为单一。开发人员广泛使用通常是基于串口或是网络接口的终端( console )方式。在这个模式下,开发人员难以细致准确的观察进程间的通信。而且对于计算能力薄弱的嵌入式系统来说,在终端上打印出通信报文既会影响系统内部的运行,同时,也会使屏幕上充斥的过多的无用信息,使开发人员的分析工作无从下手。
为了解决这个问题,在嵌入式 Linux 的平台上,我们开发了一整套用于监视嵌入式系统内进程间通信的软件,用于调试我们开发的嵌入式产品。本文详细介绍了监视嵌入式系统内进程间通信的技术原理和实现监视软件的推荐方案。
监视方法的基本原理
Linux 中的 ptrace 系统调用是监视进程间通信的关键。 ptrace 为我们提供了一种观察和控制其它进程的方法。利用 ptrace ,我们可以截获正在运行的进程的所有的系统调用。所谓截获是指,监视程序可以在这些系统调用发生和退出时,获得系统调用的参数,甚至修改参数。这些系统调用包括: read , write , sendto, recv 等等。在 Linux 中,用户可以通过“ man syscalls ”来查看当前版本的 Linux 所支持的系统调用。
在我们的 Linux 嵌入式产品中, AF_UNIX 域的 socket 被广泛使用。它被用来完成进程间通信的工作。 AF_UNIX 域的 socket 的编程模型与通常的 socket 编程模型完全相同。我们的使用方法是:接收进程创建一个 AF_UNIX 域的 socket ,设定其模式为数据报( SOCK_DGRAM )。在这之后,为其绑定一个含路径的文件名,例如: /var/tmp/receive.unix 。这个文件名被内核用于标识socket。发送进程创建一个相同模式的 AF_UNIX 域的 socket 。然后,调用 sendto 向接收进程发送消息。用来标识接收进程 socket 的就是前面提到的文件名,也就是 /var/tmp/receive.unix 。而接收进程使用 recvfrom 系统调用,就可以收到发送进程发出的消息。
因此,通过 ptrace ,一旦我们接管了被监视进程的 sendto 和 recvfrom 系统调用,将使我们能够截获到使用这两个系统调用进行通信的数据。
ptrace 系统调用的定义如下:
#include <sys/ptrace.h>
long int ptrace(enum __ptrace_request request, pid_t pid,
void * addr, void * data);
它共有四个参数。 request 的值决定 ptrace 执行什么样的任务。 pid 指明被追踪的进程的 id 。 request 参数决定了是否需要一个有效的 addr 参数,还是仅用 NULL 即可。如果有必要使用有效的 addr 参数,它的含义是被追踪的进程的进程空间的偏移量。 data 类似于 addr 参数,有时也可以使用 NULL 来代替。如果它被使用,它的含义是指向一些数据,这些数据希望被放置到被监视的进程的用户空间中。
一个完整的示例代码将向我们展示监视进程间通信的技术细节和关键点。代码按前后顺序分段说明。
#include <stdio.h>
#include <stdlib.h>
#include <sys/ptrace.h>
#include <sys/wait.h>
#include <Linux/user.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <Linux/net.h>
为了在程序中使用 ptrace 系统调用,我们需要增加 ptrace.h 头文件。为了能够获得截获的系统调用的函数入参,我们需要使用 struct user_regs_struct 结构。它在 user.h 中被定义。由于在程序中使用了信号,因此,我们也需要 wait.h 。我们要监视通信动作, socket.h 和 un.h 则是必不可少的。
下面是程序的入口主函数:
int main (int argc, char *argv[])
{
int status;
int syscall_entry = 0;
int traced_process;
struct user_regs_struct u_in;
status 用于记录被监视进程的状态变化; syscall_entry 记录被监视进程当前是进入系统调用,还是从系统调用中返回; u_in 用来获得截获的系统调用的参数; traced_process 则是被监视进程的 PID 值。
traced_process = atoi(argv[1]); /* 从命令行得到监视进程的PID */
ptrace(PTRACE_ATTACH, traced_process, NULL, NULL);
wait(&status); /* 等待被监视进程状态变化 */
ptrace(PTRACE_SYSCALL, traced_process, NULL, NULL);
参数为 PTRACE_ATTACH 的 ptrace 对被监视进程在内核中的进程结构进行修改。使被监视进程成为当前程序的子进程。一旦被监视进程的状态发生变化, wait() 将返回。程序再次调用 ptrace 。这次的参数为 PTRACE_SYSCALL 。被监视进程的进程结构再次被修改,其 trace 标志被激活。内核将在被监视进程的每一次系统调用时,触发当前程序的运行。
While (1) {
/* 等待被监视程序调用系统调用或是发生其它状态变化 */
wait(&status);
/* 如果被监视进程退出,函数返回真。程序退出 */
if ( WIFEXITED(status) )
break;
ptrace(PTRACE_GETREGS, traced_process, 0, &u_in);
if (u_in.orig_eax == 102 && u_in.ebx == SYS_SENDTO) {
if (syscall_entry == 0) { /* syscall entry */
insyscall = 1;
printf("call sendto()n");
}
else { /* Syscall exit */
Syscall_entry = 0;
}
}
ptrace(PTRACE_SYSCALL, traced_process, NULL, NULL);
} /* while */
return 0;
} /* main */
被监视进程的 trace 标志被激活后,它的每一次系统调用都会被内核检查。我们程序也随之被内核用信号通知。使用参数 PTRACE_GETREGS 的 ptrace() 将获得截获的系统调用的参数。最重要的参数是系统调用号。它保存在了 u_in.orig_eax 中。通过系统调用号,我们可以确定发生的是那一个系统调用。系统调用号可以在 Linux 的源代码中查找。它的定义在 Linux-source-2.6.xx/arch/x86/kernel/syscall_table_32.S 中。它的部分代码如下所示:
.long sys_fstatfs /* 100 */
.long sys_ioperm
.long sys_socketcall
.long sys_syslog
在这里,我们最关心的是 sendto 系统调用。在 Linux 的内核中, sendto 的真实入口是 socketcall 系统调用。它是 bind , sendto 等socket相关系统调用的入口。在这个系统调用中,通过一个 call number 来区分出 bind , sendto 等不同的子系统调用。在我们的程序中,这个 call number 保存在 u_in.ebx 中。 从上面的 syscall_table_32.S 示例代码就可以看出, socketcall 的系统调用号是102(从100向下数两行)。而 call number 则在 net.h 有定义,我们关心的 sendto 的 call number 被定义为 SYS_SENDTO ,其绝对值为11。有了这两个重要的数据,我们的程序据此判断当前发生的系统调用是否为 sendto 。这一点表现为代码:
if (u_in.orig_eax == 102 && u_in.ebx == SYS_SENDTO)
被监视进程进入系统调用和退出系统调用时,都会触发 wait() 返回,使我们的程序有机会运行。因此,我们需要使用 syscall_entry 来记录当前时刻是被监视进程进入系统调用,还是退出系统调用。这是一个开关量,非常容易理解。 最后,每次处理完,都需要再次调用参数为 PTRACE_SYSCALL 的 ptrace ,准备监视下一次的系统调用。
上面的程序虽然很简单,但已经可以完整的表现出利用 ptrace 截获被监视进程的 sendto 系统调用的过程。值得补充一点的是,利用 ptrace 也可以获得 sendto 向外发送的数据。
sendto 系统调用的定义是:
#include <sys/types.h>
#include <sys/socket.h>
size_t sendto(int s, const void *msg, size_t len, int flags,
const struct sockaddr *to, socket len_t tolen);
sendto 包含了六个参数,特别是 msg 参数指出了发送的数据内容。参数 to 指出了发送的目标。利用 PTRACE_PEEKDATA 参数的 ptrace ,监视程序将可以获得 sendto 的全部的六个参数。这样监视程序就完全获得了被监视进程要向外发送的数据和发送目标。具体的实现细节在此不再展开论述。请参考 man ptrace 说明手册。监视系统的体系和应用
利用上面讨论的技术,我们开发了可以运行在 mips 目标板上的监视程序,名为 ipcmsg 。它是一个命令行程序。在我们的应用环境中,它的使用方法是:
root@host:~$ ipcmsg -p pid -l xxx.xxx.xxx.xxx -b 6000
pid 是被监视进程的 pid ,可以通过 ps 命令获得。 -l 参数后面指定 PC 主机的 IP 地址。 -b 参数指明了接收的端口号。
最初进行监视时, ipcmsg 是没有 IP 地址和端口号参数的。所有信息是输出到串口控制台中。这既影响了运行的效率(大量的在串口上的输出会影响目标板的运行速度),也不利于信息的处理。由于我们的目标板具备以太网接口,我们很容易的想到将 ipcmsg 截获的数据包转发到 PC 主机上。使用 PC 主机更便于对进程间通信的数据包进行分析。在 PC 主机上,我们使用 wireshark 这个非常流行的开源的网络报文分析软件接收来自目标板的信息。整个监视系统的架构如下图所示
在实际的使用过程中,我们使用以太网线将目标板与 PC 主机相连。然后,在目标板上启动 ipcmsg ,并为其指定监视进程的 pid 。 ipcmsg运行后,我们在PC主机上启动 wireshark 接收来自 ipcmsg 的数据包。这些数据包中包含了 mips 目标板上进程间通信的信息。利用我们为 ipcmsg 专门开发的 wireshark 插件,在 wireshark 上,我们可以详细的分解 ipcmsg 转发来的数据包,非常直观的分析进程间通信的过程和可能存在的问题。下面是 wireshark 分解 ipcmsg 数据包的实际运行图:
图2 运行图
从图中可以看到,我们从 ipcmsg 获得了进程间通信的方式,参数( path 是 AF_UNIX域 socket 地址参数),方向和内容,以及进程名称。这些信息帮助我们对嵌入式系统的运行状态进行分析。而这一切非常直观和便于操作。