简介: 本文详细描述了一种利用 ptrace 系统调用,实现嵌入式系统内部进程通信的监视方法,并提供了相应的实现方案。
复杂的嵌入式系统中,常常同时运行着相当多的进程。这些进程之间频繁的进行着大量的通信动作。进程的运行状态与这些不断发生的通信有着直接和紧密的联系。通过对进程间通信的监视,开发人员可以掌控系统内部运转的状态。发现错误时,利用获取到的进程间通信的信息,调试工程师更容易发现问题之所在。
但是,嵌入式系统与开发人员的接口往往较为单一。开发人员广泛使用通常是基于串口或是网络接口的终端( 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
中。
通过系统调用号,我们可以确定发生的是那一个系统调用。
系统调用号可以在
L
inux
的源代码中查找。它的定义在
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
这个非常流行的开源的网络报文分析软件接收来自目标板的信息。整个监视系统的架构如下图所示:
图1 架构
在实际的使用过程中,我们使用以太网线将目标板与 PC
主机相连。然后,在目标板上启动 ipcmsg
,并为其指定监视进程的 pid
。ipcmsg
运行后,我们在PC主机上启动 wireshark
接收来自 ipcmsg
的数据包。这些数据包中包含了 mips
目标板上进程间通信的信息。利用我们为 ipcmsg
专门开发的 wireshark
插件,在 wireshark
上,我们可以详细的分解 ipcmsg
转发来的数据包,非常直观的分析进程间通信的过程和可能存在的问题。下面是 wireshark
分解 ipcmsg
数据包的实际运行图:
图2 运行图
从图中可以看到,我们从 ipcmsg
获得了进程间通信的方式,参数( path
是 AF_UNIX
域 socket
地址参数),方向和内容,以及进程名称。这些信息帮助我们对嵌入式系统的运行状态进行分析。而这一切非常直观和便于操作。