Chinaunix首页 | 论坛 | 博客
  • 博客访问: 779758
  • 博文数量: 239
  • 博客积分: 60
  • 博客等级: 民兵
  • 技术积分: 1045
  • 用 户 组: 普通用户
  • 注册时间: 2009-03-22 18:25
文章分类

全部博文(239)

文章存档

2019年(9)

2018年(64)

2017年(2)

2016年(26)

2015年(30)

2014年(41)

2013年(65)

2012年(2)

分类: Python/Ruby

2016-09-12 11:25:34

一. 概述

    项目中使用Python总会由于某种原因(人力/时间/金钱等等),需要借助外部代码,幸好Python是容易扩展的语言,本文浅析三种常见扩展方式:subprocess调用外部可执行程序;ctypes调用外部DLL函数;C/C++ Extending标准库。用一个需求的三种实现方式贯穿始终,正是对应标题的“Python调用外部代码的三种方式”,该需求比“hello,world”复杂,但依然只有百行规模的代码,容易看懂,力争做好“万事开头难”中的“开头”,以后修行就看个人了,同时作为自己学习的备忘,以免隔三差五查文档或犯同样的错误。理解全文需熟悉Python和C,如果有Linux/UNIX系统编程知识更好,全文篇幅较长,不得不分三篇(上,中,下)发布,上篇包括subprocess和ctypes,中篇是C/C++ Extending,下篇包含文中所有代码压缩包的附件。

此处“外部代码”是指:
1. 
本地可执行程序,通常是原生二进制文件或解释器文本文件。
2. 
本地动态链接库,通常由C/C++编译而成。

先简要概括下这三种调用方式:
1. 
subprocess。这是一个标准库,可以运行外部程序,接口简单易用,容易跨平台。不过灵活性欠佳,因为Python无法控制外部程序的运行逻辑,只适合做简单的输入输出交互。
2. 
ctypes。这是一个标准库,可以加载动态链接库,借助ctypes提供的wrapper,可以方便的使用C库函数和系统调用,功能和灵活性大增,做hack的利器。不过毕竟是按照C API的方式利用这类接口,用动态解释语言去适配静态编译语言接口,别扭程度可想而知。
3. 
extending。这是一种实现方式,全称是Python C/C++ Extending,即用C/C++写Python模块或定制Python解释器,这是扩展Python的终极方案,可以随心所欲,将项目已有的C/C++接口包装为Python模块,或将有性能瓶颈的纯Python模块用C/C++重写,或为Python增加内置函数和对象。不过这是扩展Python中最复杂的方式,将模块与CPython绑定,而与JPython/Iron Python/PyPy无缘了,关键是“来到危险的C/C++世界”!

二. 系统要求

关于测试环境。本人的系统是Fedora 20 (Linux 3.13, gcc 4.8, Python 3.3),如果kernel/gcc/Python的版本低于我的测试环境,示例代码可能要微调。
关于C++。后文中所有出现“C”的地方,都可以用“C++”代替,实现原理不变,但本文的实现用C语法,如果要移植到C++,需要做几点语法上的调整,留给感兴趣的读者。
关于示例代码。如果先前没有接触过本文介绍的三种方式,快速进入状态的方法就是下载附件中的代码,编译运行调试修改,同时看文档找资料,示例虽简单,但属于麻雀虽小五脏俱全的类型,有助于在这三个领域中继续深入。

三. 需求描述

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

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

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

实现。

1. 准备C程序,作为外部代码。

点击(此处)折叠或打开(notify1.c)

  1. #include <limits.h>
  2. #include <stdio.h>
  3. #include <stdlib.h>
  4. #include <string.h>
  5. #include <sys/inotify.h>
  6. #include <sys/time.h>
  7. #include <time.h>
  8. #include <unistd.h>

  9. #define EVENT_SIZE_MAX (sizeof(struct inotify_event) + NAME_MAX + 1)
  10. #define EVENT_SIZE_MIN (sizeof(struct inotify_event))
  11. #define ARRAY_SIZE(a) (sizeof(a)/sizeof(a[0]))

  12. typedef struct {
  13.     char *name;
  14.     uint32_t value;
  15. } mask_event;

  16. static const mask_event MASK_EVENTS[] = {
  17.     {"IN_ACCESS", IN_ACCESS},
  18.     {"IN_ATTRIB", IN_ATTRIB},
  19.     {"IN_CLOSE_NOWRITE", IN_CLOSE_NOWRITE},
  20.     {"IN_CLOSE_WRITE", IN_CLOSE_WRITE},
  21.     {"IN_CREATE", IN_CREATE},
  22.     {"IN_DELETE", IN_DELETE},
  23.     {"IN_DELETE_SELF", IN_DELETE_SELF},
  24.     {"IN_DONT_FOLLOW", IN_DONT_FOLLOW},
  25.     {"IN_EXCL_UNLINK", IN_EXCL_UNLINK},
  26.     {"IN_IGNORED", IN_IGNORED},
  27.     {"IN_ISDIR", IN_ISDIR},
  28.     {"IN_MASK_ADD", IN_MASK_ADD},
  29.     {"IN_MODIFY", IN_MODIFY},
  30.     {"IN_MOVED_FROM", IN_MOVED_FROM},
  31.     {"IN_MOVED_TO", IN_MOVED_TO},
  32.     {"IN_MOVE_SELF", IN_MOVE_SELF},
  33.     {"IN_ONESHOT", IN_ONESHOT},
  34.     {"IN_ONLYDIR", IN_ONLYDIR},
  35.     {"IN_OPEN", IN_OPEN},
  36.     {"IN_Q_OVERFLOW", IN_Q_OVERFLOW},
  37.     {"IN_UNMOUNT", IN_UNMOUNT}
  38. };

  39. void print_event(const char *target, const struct inotify_event *ev);

  40. int main(int argc, char *argv[])
  41. {
  42.     /* at least one event available in the buffer */
  43.     char ev_buffer[EVENT_SIZE_MAX];
  44.     ssize_t len, offset;
  45.     struct inotify_event *p = NULL;
  46.     const char *pathname = NULL;
  47.     int fd, wd;

  48.     if (argc != 2) {
  49.         fprintf(stderr, "usage: notify path\n");
  50.         exit(EXIT_FAILURE);
  51.     }
  52.     pathname = argv[1];

  53.     fd = inotify_init();
  54.     if (fd == -1) {
  55.         perror("inotify_init");
  56.         exit(EXIT_FAILURE);
  57.     }
  58.     wd = inotify_add_watch(fd, pathname, IN_ALL_EVENTS);
  59.     if (wd == -1) {
  60.         perror("inotify_add_watch");
  61.         exit(EXIT_FAILURE);
  62.     }

  63.     while (1) {
  64.         len = read(fd, ev_buffer, sizeof(ev_buffer));
  65.         if (len < (ssize_t)EVENT_SIZE_MIN) {
  66.             perror("read");
  67.             break;
  68.         }

  69.         offset = 0;
  70.         while (offset < len) {
  71.             p = (struct inotify_event*)(ev_buffer + offset);
  72.             print_event(pathname, p);
  73.             offset += sizeof(*p) + p->len;
  74.         }
  75.     }
  76.     inotify_rm_watch(fd, wd);
  77.     close(fd);

  78.     return 0;
  79. }

  80. void print_event(const char *target, const struct inotify_event *ev)
  81. {
  82.     struct timeval tv;
  83.     struct tm *lt;
  84.     const char *name = ev->len > 1 ? ev->name : target;
  85.     size_t idx;
  86.     const mask_event *me;

  87.     gettimeofday(&tv, NULL);
  88.     lt = localtime(&tv.tv_sec);
  89.     printf("%02d:%02d:%02d,%03ld file(%s) ",
  90.             lt->tm_hour, lt->tm_min, lt->tm_sec, tv.tv_usec/1000, name);
  91.     for (idx = 0; idx < ARRAY_SIZE(MASK_EVENTS); ++idx) {
  92.         me = &MASK_EVENTS[idx];
  93.         if (ev->mask & me->value) printf("%s ", me->name);
  94.     }
  95.     printf("\n");
  96. }
如果对Linux inotify API不熟悉,可以man inotify或查看在线文档。
代码行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代码:
  1. bash-4.2 $make notify1
  2. gcc -g -O2 -Wall notify1.c -o notify1
Python shell中直接调用,监控当前目录的变化,可以看到setup.py的重命名操作:
  1. (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)

  1. from subprocess import Popen, PIPE
  2. import itertools
  3. import sys

  4. def main():
  5.     if len(sys.argv) != 2:
  6.         sys.exit('usage: {} cnt'.format(sys.argv[0]))
  7.     cnt = int(sys.argv[1])

  8.     p = Popen('cat', shell=True, stdin=PIPE, stdout=PIPE, close_fds=True)
  9.     for e in itertools.repeat(b'x' * 1023 + b'\n', cnt):
  10.         p.stdin.write(e)
  11.     p.stdin.close()
  12.     print(len([e for e in p.stdout]))

  13. if __name__ == '__main__':
  14.     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)

  1. from ctypes import CDLL, create_string_buffer
  2. import datetime
  3. import os
  4. import struct
  5. import sys

  6. MASK_EVENTS = [
  7.     ("IN_ACCESS", 0x00000001),
  8.     ("IN_ATTRIB", 0x00000004),
  9.     ("IN_CLOSE_NOWRITE", 0x00000010),
  10.     ("IN_CLOSE_WRITE", 0x00000008),
  11.     ("IN_CREATE", 0x00000100),
  12.     ("IN_DELETE", 0x00000200),
  13.     ("IN_DELETE_SELF", 0x00000400),
  14.     ("IN_DONT_FOLLOW", 0x02000000),
  15.     ("IN_EXCL_UNLINK", 0x04000000),
  16.     ("IN_IGNORED", 0x00008000),
  17.     ("IN_ISDIR", 0x40000000),
  18.     ("IN_MASK_ADD", 0x20000000),
  19.     ("IN_MODIFY", 0x00000002),
  20.     ("IN_MOVED_FROM", 0x00000040),
  21.     ("IN_MOVED_TO", 0x00000080),
  22.     ("IN_MOVE_SELF", 0x00000800),
  23.     ("IN_ONESHOT", 0x80000000),
  24.     ("IN_ONLYDIR", 0x01000000),
  25.     ("IN_OPEN", 0x00000020),
  26.     ("IN_Q_OVERFLOW", 0x00004000),
  27.     ("IN_UNMOUNT", 0x00002000),
  28. ]
  29. IN_ALL_EVENTS = 0x00000fff

  30. # Maximum length of a filename. not including the terminating null
  31. NAME_MAX = os.pathconf('.', 'PC_NAME_MAX')
  32. # unpack inotify_event
  33. EV_FMT = struct.Struct('@i3I')
  34. # limit of inotify_event
  35. EV_LEN_MAX = EV_FMT.size + NAME_MAX + 1
  36. EV_LEN_MIN = EV_FMT.size

  37. def print_event(filename, mask):
  38.     now = datetime.datetime.now()
  39.     print('{:02}:{:02}:{:02},{:03} file({file}) {mask}'.format(
  40.             now.hour, now.minute, now.second, now.microsecond//1000,
  41.             file=str(filename, 'utf-8'),
  42.             mask=' '.join(k for k,v in MASK_EVENTS if mask & v)))

  43. def main():
  44.     if len(sys.argv) != 2:
  45.         sys.exit('usage: {} path'.format(sys.argv[0]))
  46.     pathname = bytes(sys.argv[1], 'utf-8')
  47.     libc = CDLL('libc.so.6')

  48.     fd = libc.inotify_init(None)
  49.     if fd == -1:
  50.         libc.perror(b'inotify_init')
  51.         sys.exit()
  52.     wd = libc.inotify_add_watch(fd, pathname, IN_ALL_EVENTS)
  53.     if wd == -1:
  54.         libc.perror(b'inotify_add_watch')
  55.         sys.exit()

  56.     ev_buffer = create_string_buffer(EV_LEN_MAX)
  57.     while True:
  58.         ret = libc.read(fd, ev_buffer, EV_LEN_MAX)
  59.         if ret < EV_LEN_MIN:
  60.             libc.perror(b'read')
  61.             break

  62.         offset = 0
  63.         while offset < ret:
  64.             wd, mask, cookie, elen = EV_FMT.unpack_from(ev_buffer, offset)
  65.             offset += EV_FMT.size
  66.             filename = ev_buffer[offset: offset + elen].rstrip(b'\x00')
  67.             print_event(filename if filename else pathname, mask)
  68.             offset += elen

  69.     libc.inotify_rm_watch(fd, wd)
  70.     libc.close(fd)

  71. if __name__ == '__main__':
  72.     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实现观察目录内文件变化。
  1. (py3) bash-4.2 $python notify2.py .
  2. 22:22:35,739 file(.) IN_ISDIR IN_OPEN
  3. 22:22:35,739 file(.) IN_CLOSE_NOWRITE IN_ISDIR
  4. 22:22:44,038 file(.) IN_ISDIR IN_OPEN
  5. 22:22:44,038 file(.) IN_CLOSE_NOWRITE IN_ISDIR
  6. 22:22:44,038 file(Makefile) IN_OPEN
  7. 22:22:44,038 file(Makefile) IN_ACCESS
  8. 22:22:44,038 file(Makefile) IN_CLOSE_NOWRITE
  9. 22:22:44,850 file(notify1.c) IN_OPEN
  10. 22:22:44,850 file(notify1.c) IN_ACCESS
  11. 22:22:44,850 file(notify1.c) IN_CLOSE_NOWRITE
  12. 22:22:47,223 file(notify1) IN_CREATE
  13. 22:22:47,223 file(notify1) IN_OPEN
  14. 22:22:47,429 file(notify1) IN_CLOSE_WRITE
  15. 22:22:47,438 file(notify1) IN_OPEN
  16. 22:22:47,438 file(notify1) IN_MODIFY
  17. 22:22:47,438 file(notify1) IN_MODIFY
  18. ......
  19. 22:22:47,441 file(notify1) IN_ACCESS
  20. 22:22:47,441 file(notify1) IN_MODIFY
  21. 22:22:47,441 file(notify1) IN_CLOSE_WRITE
  22. 22:22:47,441 file(notify1) IN_ATTRIB
  23. 22:23:07,730 file(.) IN_ISDIR IN_OPEN
  24. 22:23:07,730 file(.) IN_CLOSE_NOWRITE IN_ISDIR
  25. 22:23:07,730 file(Makefile) IN_OPEN
  26. 22:23:07,730 file(Makefile) IN_ACCESS
  27. 22:23:07,730 file(Makefile) IN_CLOSE_NOWRITE
  28. 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的缓冲期溢出和内存泄漏,但可能出问题的地方必将出现问题,出来混总是要还的。


(更多内容参见中篇)

阅读(3271) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~