python函数调用的三种方式_浅析Python调用外部代码的三种方式(上篇)

简单说就是监控一个目录内文件的变化(访问,打开,关闭,数据修改,属性修改,移动,删除等等),然后打印出(时间,文件名,相关事件)。方案也是现成的,直接用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的缓冲期溢出和内存泄漏,但可能出问题的地方必将出现问题,出来混总是要还的。

(更多内容参见中篇)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值