简单说就是监控一个目录内文件的变化(访问,打开,关闭,数据修改,属性修改,移动,删除等等),然后打印出(时间,文件名,相关事件)。方案也是现成的,直接用Linux的inotify机制,比如BSD的kqueue也提供了类似功能,但Python标准库没有inotify API,这也正好是Python需调用外部代码的场景之一。

四. subprocess调用外部可执行程序

场景。需求已由外部程序实现,Python只需要做简单输入输出的整合。

实现。

1. 准备C程序,作为外部代码。
点击(此处)折叠或打开(notify1.c)
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#define EVENT_SIZE_MAX (sizeof(struct inotify_event) + NAME_MAX + 1)
#define EVENT_SIZE_MIN (sizeof(struct inotify_event))
#define ARRAY_SIZE(a) (sizeof(a)/sizeof(a[0]))
typedef struct {
char *name;
uint32_t value;
} mask_event;
static const mask_event MASK_EVENTS[] = {
{"IN_ACCESS", IN_ACCESS},
{"IN_ATTRIB", IN_ATTRIB},
{"IN_CLOSE_NOWRITE", IN_CLOSE_NOWRITE},
{"IN_CLOSE_WRITE", IN_CLOSE_WRITE},
{"IN_CREATE", IN_CREATE},
{"IN_DELETE", IN_DELETE},
{"IN_DELETE_SELF", IN_DELETE_SELF},
{"IN_DONT_FOLLOW", IN_DONT_FOLLOW},
{"IN_EXCL_UNLINK", IN_EXCL_UNLINK},
{"IN_IGNORED", IN_IGNORED},
{"IN_ISDIR", IN_ISDIR},
{"IN_MASK_ADD", IN_MASK_ADD},
{"IN_MODIFY", IN_MODIFY},
{"IN_MOVED_FROM", IN_MOVED_FROM},
{"IN_MOVED_TO", IN_MOVED_TO},
{"IN_MOVE_SELF", IN_MOVE_SELF},
{"IN_ONESHOT", IN_ONESHOT},
{"IN_ONLYDIR", IN_ONLYDIR},
{"IN_OPEN", IN_OPEN},
{"IN_Q_OVERFLOW", IN_Q_OVERFLOW},
{"IN_UNMOUNT", IN_UNMOUNT}
};
void print_event(const char *target, const struct inotify_event *ev);
int main(int argc, char *argv[])
{
/* at least one event available in the buffer */
char ev_buffer[EVENT_SIZE_MAX];
ssize_t len, offset;
struct inotify_event *p = NULL;
const char *pathname = NULL;
int fd, wd;
if (argc != 2) {
fprintf(stderr, "usage: notify path\n");
exit(EXIT_FAILURE);
}
pathname = argv[1];
fd = inotify_init();
if (fd == -1) {
perror("inotify_init");
exit(EXIT_FAILURE);
}
wd = inotify_add_watch(fd, pathname, IN_ALL_EVENTS);
if (wd == -1) {
perror("inotify_add_watch");
exit(EXIT_FAILURE);
}
while (1) {
len = read(fd, ev_buffer, sizeof(ev_buffer));
if (len < (ssize_t)EVENT_SIZE_MIN) {
perror("read");
break;
}
offset = 0;
while (offset < len) {
p = (struct inotify_event*)(ev_buffer + offset);
print_event(pathname, p);
offset += sizeof(*p) + p->len;
}
}
inotify_rm_watch(fd, wd);
close(fd);
return 0;
}
void print_event(const char *target, const struct inotify_event *ev)
{
struct timeval tv;
struct tm *lt;
const char *name = ev->len > 1 ? ev->name : target;
size_t idx;
const mask_event *me;
gettimeofday(&tv, NULL);
lt = localtime(&tv.tv_sec);
printf("%02d:%02d:%02d,%03ld file(%s) ",
lt->tm_hour, lt->tm_min, lt->tm_sec, tv.tv_usec/1000, name);
for (idx = 0; idx < ARRAY_SIZE(MASK_EVENTS); ++idx) {
me = &MASK_EVENTS[idx];
if (ev->mask & me->value) printf("%s ", me->name);
}
printf("\n");
}

如果对Linux inotify API不熟悉,可以man inotify或查看在线文档http://man7.org/linux/man-pages/man7/inotify.7.html。

代码行72~73:struct inotify_event表示的真实内容是变长的,但受限于系统文件名长度,可以知道最大长度EVENT_SIZE_MAX,传入的ev_buffer至少保证放入一个事件,否则在新内核(kernel 2.6.21以后)下会报错,如果有新事件,返回的len至少应该是EVENT_SIZE_MIN。

代码行85~86:这两句在测试中是没执行过的,通过终端手动CTRL+C,SIGINT直接终止进程了,系统可以保证清理进程打开的fd,此处仅为展示应有逻辑,但在一些实际要求可重入的环境下,必须考虑合理的清理操作。

代码行103~106:同一个ev下可能含有多个mask位,需要循环处理。

2. subprocess调用。主要工作被外部代码做了,Python只是简单传参和输出就OK,在交互shell演示结果。

编译上述C代码:
bash-4.2 $make notify1
gcc -g -O2 -Wall notify1.c -o notify1
Python shell中直接调用,监控当前目录的变化,可以看到setup.py的重命名操作:
(py3) bash-4.2 $python
Python 3.3.2 (default, Feb 11 2014, 10:35:02)
[GCC 4.8.2 20131212 (Red Hat 4.8.2-7)] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import subprocess
>>> subprocess.check_call(['./notify1', '.'])
13:29:57,856 file(setup.py) IN_MOVED_FROM
13:29:57,856 file(setup.py.bak) IN_MOVED_TO
......
13:30:04,402 file(.) IN_CLOSE_NOWRITE IN_ISDIR
13:30:04,403 file(.) IN_ISDIR IN_OPEN
13:30:04,403 file(.) IN_CLOSE_NOWRITE IN_ISDIR
......
13:30:06,555 file(.) IN_CLOSE_NOWRITE IN_ISDIR
13:30:08,600 file(setup.py.bak) IN_MOVED_FROM
13:30:08,600 file(setup.py) IN_MOVED_TO
备忘:
***参数“shell=True”表示调用系统shell执行命令, 如果执行内容不可控,会有重大安全隐患,避免使用。
***pipe相关的阻塞。实践中可能会用到更复杂的交互,比如与子进程的stdin/stdout/stderr交互,此时要注意可能的死锁,下面是一个简单例子
点击(此处)折叠或打开(pipe.py)
from subprocess import Popen, PIPE
import itertools
import sys
def main():
if len(sys.argv) != 2:
sys.exit('usage: {} cnt'.format(sys.argv[0]))cnt = int(sys.argv[1])p = Popen('cat', shell=True, stdin=PIPE, stdout=PIPE, close_fds=True)
for e in itertools.repeat(b'x' * 1023 + b'\n', cnt):p.stdin.write(e)
p.stdin.close()
print(len([e for e in p.stdout]))
if __name__ == '__main__':
main()

程序逻辑:每次向cat的stdin写1k数据,若干次后,在从cat的stdout读取回来。如果cnt只是几十次看不出什么问题,几百次后就会发生死锁。

问题原因:内核为pipe分配的空间是有限的,目前一个pipe为64k,双向128k,加上一些用户空间缓存,处理大量数据时很容易爆掉。

解决方案:一种办法就是用另外一个进程或线程处理stdout,工作流就像shell的pipe,连续不断,不会卡壳。

五. ctypes调用外部DLL函数

场景。无可用Python模块,只好求助于C API,搞定燃眉之急。

实现。

1. 已有第四节subprocess C版本的实现,而ctypes解决问题的途径就是调用C函数,下面的Python实现就水到渠成了。

点击(此处)折叠或打开(notify2.py)
from ctypes import CDLL, create_string_buffer
import datetime
import os
import struct
import sys
MASK_EVENTS = [
("IN_ACCESS", 0x00000001),
("IN_ATTRIB", 0x00000004),
("IN_CLOSE_NOWRITE", 0x00000010),
("IN_CLOSE_WRITE", 0x00000008),
("IN_CREATE", 0x00000100),
("IN_DELETE", 0x00000200),
("IN_DELETE_SELF", 0x00000400),
("IN_DONT_FOLLOW", 0x02000000),
("IN_EXCL_UNLINK", 0x04000000),
("IN_IGNORED", 0x00008000),
("IN_ISDIR", 0x40000000),
("IN_MASK_ADD", 0x20000000),
("IN_MODIFY", 0x00000002),
("IN_MOVED_FROM", 0x00000040),
("IN_MOVED_TO", 0x00000080),
("IN_MOVE_SELF", 0x00000800),
("IN_ONESHOT", 0x80000000),
("IN_ONLYDIR", 0x01000000),
("IN_OPEN", 0x00000020),
("IN_Q_OVERFLOW", 0x00004000),
("IN_UNMOUNT", 0x00002000),
]
IN_ALL_EVENTS = 0x00000fff
# Maximum length of a filename. not including the terminating null
NAME_MAX = os.pathconf('.', 'PC_NAME_MAX')
# unpack inotify_event
EV_FMT = struct.Struct('@i3I')
# limit of inotify_event
EV_LEN_MAX = EV_FMT.size + NAME_MAX + 1
EV_LEN_MIN = EV_FMT.size
def print_event(filename, mask):
now = datetime.datetime.now()
print('{:02}:{:02}:{:02},{:03} file({file}) {mask}'.format(
now.hour, now.minute, now.second, now.microsecond//1000,
file=str(filename, 'utf-8'),
mask=' '.join(k for k,v in MASK_EVENTS if mask & v)))
def main():
if len(sys.argv) != 2:
sys.exit('usage: {} path'.format(sys.argv[0]))
pathname = bytes(sys.argv[1], 'utf-8')
libc = CDLL('libc.so.6')
fd = libc.inotify_init(None)
if fd == -1:
libc.perror(b'inotify_init')
sys.exit()
wd = libc.inotify_add_watch(fd, pathname, IN_ALL_EVENTS)
if wd == -1:
libc.perror(b'inotify_add_watch')
sys.exit()
ev_buffer = create_string_buffer(EV_LEN_MAX)
while True:
ret = libc.read(fd, ev_buffer, EV_LEN_MAX)
if ret < EV_LEN_MIN:
libc.perror(b'read')
break
offset = 0
while offset < ret:
wd, mask, cookie, elen = EV_FMT.unpack_from(ev_buffer, offset)
offset += EV_FMT.size
filename = ev_buffer[offset: offset + elen].rstrip(b'\x00')
print_event(filename if filename else pathname, mask)
offset += elen
libc.inotify_rm_watch(fd, wd)
libc.close(fd)
if __name__ == '__main__':
main()

2. 代码分析。因为本文是讨论“Python调用外部代码”,而这是第一个Python和外部代码有多次交互的例子,需要分析几个关键的地方。不管Python和C语法上的差异,大家可能注意到一些实现细节的改变,这些地方恰好能体现出ctypes功能与局限。

行7~30:定义了所有mask的名称和数值,对比C实现的那个相似结构,这里hardcode了数值,因为Python不认识C的宏定义,这些数值只能人肉从inotify.h中提取,如果实践中遇到更复杂的头文件,而所需要的常量包含在众多的条件编译选项中,建议还是写几行C代码来提取这些数值。

行32~38:依然在处理C中的常量,C的NAME_MAX通过Python的os提取,C的struct inotify_event成员分布可以由Python的struct模拟,初步可以看出ctypes的代价了,如果这类常量定义有任何改变,C实现只要一次重编就搞定,Python实现就是噩梦了!

行51:加载glibc,搜索规则与C的dlopen一样,含有“/”则按照路径检索,否则通过程序代码段(DT_RPATH,gcc编译时指定)-->环境变量(LD_LIBRARY_PATH)-->系统库(/lib, /usr/lib),其实中间还有其他环节,但实践中通过这三个层次应该定位到自己的DLL了。

行57:Python bytes对应C const char *,所以此处pathname必须提前从str转换为bytes。

行64:read系统调用的ev_buffer会写入内容,Python没有内置类型可以容易转换为这种结构,所以ctypes定义了create_string_buffer。

行73:提取struct inotify_event的name成员时,要去掉末尾由于地址对齐填充的‘\0’字节。

3. 输出分析。功能上与第四节的C版本完全一致,只是此处观察目录中几个不同的事件,用make在目录中编译C版本实现,用Python实现观察目录内文件变化。

(py3) bash-4.2 $python notify2.py .
22:22:35,739 file(.) IN_ISDIR IN_OPEN
22:22:35,739 file(.) IN_CLOSE_NOWRITE IN_ISDIR
22:22:44,038 file(.) IN_ISDIR IN_OPEN
22:22:44,038 file(.) IN_CLOSE_NOWRITE IN_ISDIR
22:22:44,038 file(Makefile) IN_OPEN
22:22:44,038 file(Makefile) IN_ACCESS
22:22:44,038 file(Makefile) IN_CLOSE_NOWRITE
22:22:44,850 file(notify1.c) IN_OPEN
22:22:44,850 file(notify1.c) IN_ACCESS
22:22:44,850 file(notify1.c) IN_CLOSE_NOWRITE
22:22:47,223 file(notify1) IN_CREATE
22:22:47,223 file(notify1) IN_OPEN
22:22:47,429 file(notify1) IN_CLOSE_WRITE
22:22:47,438 file(notify1) IN_OPEN
22:22:47,438 file(notify1) IN_MODIFY
22:22:47,438 file(notify1) IN_MODIFY
......
22:22:47,441 file(notify1) IN_ACCESS
22:22:47,441 file(notify1) IN_MODIFY
22:22:47,441 file(notify1) IN_CLOSE_WRITE
22:22:47,441 file(notify1) IN_ATTRIB
22:23:07,730 file(.) IN_ISDIR IN_OPEN
22:23:07,730 file(.) IN_CLOSE_NOWRITE IN_ISDIR
22:23:07,730 file(Makefile) IN_OPEN
22:23:07,730 file(Makefile) IN_ACCESS
22:23:07,730 file(Makefile) IN_CLOSE_NOWRITE
22:23:07,731 file(notify1) IN_DELETE

上面的输出就是在另一个窗口先后执行make notify1和make clean的结果,很容易猜到gcc生成目标文件的最后一步就是修改可执行权限(22:22:47,441 file(notify1) IN_ATTRIB)

备忘:

***int,str,bytes可以直接传入到C API,int会做适当的类型转换,str对应const wchar_t *,bytes对应const char *,其他一切Python类型,都要转换为ctypes的类型才能传入C API

***Python的Immutable类型规则(特别是str,bytes),也要被C API遵守,比如bytes对应的const char *而不能是char *,违反了该规则不见得立即有问题,就像C的缓冲期溢出和内存泄漏,但可能出问题的地方必将出现问题,出来混总是要还的。

(更多内容参见中篇)