通过os.pipe方法可以创建一个管道,管道有一个读端和一个写入端。进程w从管道的写入端写入数据,进程r就可以从读端将数据给读取出来。pipe方法会返回一个元组,元组中包含两个文件描述符,元组的第一个描述符r_fd可以用于访问管道的读端,另一个w_fd则可以访问管道的写入端...

    页面导航: 前言:

    本篇文章由网友“小黑”投稿发布。

    这篇文章是接着上一篇的内容来写的。

    相关英文教程的下载链接,以及Python 2.7.8的C源代码的下载链接,请参考之前"Python基本的I/O操作 (一)"的内容。

    文章中的脚本代码主要是通过Slackware Linux系统中的python 2.7.8的版本来进行测试的。部分代码是通过Mac OS X中的Python 2.6.1的版本以及win7中的Python 2.7.9的版本来测试的。

    文章中的链接,可能都需要通过代理才能正常访问。

os模块的pipe方法:

    pipe(管道)是进程间通信的一种方式。如下图所示:


图1

    通过os.pipe方法可以创建一个管道,管道有一个读端和一个写入端。进程w从管道的写入端写入数据,进程r就可以从读端将数据给读取出来。pipe方法会返回一个元组,元组中包含两个文件描述符,元组的第一个描述符r_fd可以用于访问管道的读端,另一个w_fd则可以访问管道的写入端。下面是一个简单的例子:

# pipe.py
import os, sys

print "I'm going to fork now - the child will write something to a pipe, and the parent will read it back"

r, w = os.pipe() # these are file descriptors, not file objects

pid = os.fork()
if pid:
    # we are the parent
    os.close(w) # use os.close() to close a file descriptor
    r = os.fdopen(r) # turn r into a file object
    print "parent: reading"
    txt = r.read()
    os.waitpid(pid, 0) # make sure the child process gets cleaned up
else:
    # we are the child
    os.close(r)
    w = os.fdopen(w, 'w')
    print "child: writing"
    w.write("here's some text from the child")
    w.close()
    print "child: closing"
    sys.exit(0)

print "parent: got it; text =", txt


    这段代码的执行结果如下:

black@slack:~/test/pipe$ python pipe.py
I'm going to fork now - the child will write something to a pipe, and the parent will read it back
parent: reading
child: writing
child: closing
parent: got it; text = here's some text from the child
black@slack:~/test/pipe$ 


    在pipe.py脚本中,会先通过os.fork创建一个子进程,接着在子进程中对父进程创建的管道的写入端,写入一段字符串数据。父进程就可以从管道的读端,将这段字符串给读取并显示出来。os.fork在创建子进程时,子进程会继承父进程的文件描述符表,如下图所示:


图2

    file descriptor(文件描述符)其实就是fd array数组的索引值,fd array中存储了file description的pointer(内核中的指针值),内核会为每个打开的文件创建一个file description(内核中的一种C结构)。file description中存储了文件的状态信息,文件的当前指针偏移信息,inode节点号等信息。

    子进程会从父进程那拷贝一份fd array,因此,父进程打开的管道描述符可以被子进程访问到,同时,子进程使用close方法关闭自己的描述符时,只会清理掉自己的fd array里的pointer,不会对父进程的fd array造成影响。同理,父进程关闭掉的文件描述符,也只会对父进程的fd array产生影响,对子进程的相同值的文件描述符不会产生影响。

    因此,在上面pipe.py脚本中,子进程使用os.close(r)关闭掉管道的读描述符后,父进程依然可以对管道的读端进行读操作。同理,父进程使用os.close(w)关闭掉管道的写入端描述符后,子进程依然可以对管道的写入端进行写入操作。

    pipe管道的写入端可以被多个进程写入,例如下面这个例子:

# pipe2.py
import os, sys, time

print "the child and child2 will write something to a pipe, and the parent will read it back"

r, w = os.pipe() # these are file descriptors, not file objects

pid = os.fork()
if pid:
    # we are the parent
    #os.close(w) # use os.close() to close a file descriptor
    ro = os.fdopen(r) # turn r into a file object
    print "parent: reading"
    txt = ro.readline()
    pid2 = os.fork()
    if(pid2):
        os.close(w)
        print "parent: reading"
        txt2 = ro.read()
    else:
        #os.close(r)
        w = os.fdopen(w, 'w')
        print "child2: writing"
        w.write("here's some text from the child2")
        w.close()
        print "child2: closing"
        sys.exit(0)
    os.waitpid(pid, 0) # make sure the child process gets cleaned up
    os.waitpid(pid2, 0) # make sure the child2 process gets cleaned up
else:
    # we are the child
    os.close(r)
    w = os.fdopen(w, 'w')
    print "child: writing"
    w.write("here's some text from the child\n")
    w.close()
    print "child: closing"
    sys.exit(0)

print "parent: got it; text =", txt
print "parent: got it; text2 =", txt2


    这段代码的执行结果如下:

black@slack:~/test/pipe$ python pipe2.py
the child and child2 will write something to a pipe, and the parent will read it back
parent: reading
child: writing
parent: reading
child: closing
child2: writing
child2: closing
parent: got it; text = here's some text from the child

parent: got it; text2 = here's some text from the child2
black@slack:~/test/pipe$ 


    上面的child和child2两个子进程,分别向管道中写入数据,parent则从读端将这些数据依次读取出来。

    管道的读端也可以被多个进程读取,例如下面这个例子:

# pipe3.py
import os, sys, time, random

print "I'm going to fork now - the child will write something to a pipe, and the parent will read it back"

r, w = os.pipe() # these are file descriptors, not file objects

pid = os.fork()
if pid:
    # we are the parent
    pid2 = os.fork()
    if(pid2):
        # we are the parent
        pid3 = os.fork()
        if(pid3):
            # we are the parent
            for i in range(11):
                rdata = os.read(r, 1024)
                print 'parent got:', '<'+rdata.rstrip(' ')+'>'
                time.sleep(random.uniform(0.5, 1.5))
        else:
            # we are child 3
            os.close(w)
            for i in range(11):
                rdata = os.read(r, 1024)
                print 'child3 got:', '<'+rdata.rstrip(' ')+'>'
                time.sleep(random.uniform(0.5, 1.5))
            os.close(r)
            print "child3: closing"
            sys.exit(0)
    else:
        # we are child 2
        os.close(r)
        for i in range(11):
            print 'child2 write.'.rjust(25)
            os.write(w, "I'm from child2, i:%d " % i)
            time.sleep(random.uniform(0.5, 1.5))
        os.close(w)
        print "child2: closing"
        sys.exit(0)
else:
    # we are the child
    os.close(r)
    for i in range(11):
        print 'child write.'.rjust(24)
        os.write(w, "I'm from child, i:%d " % i)
        time.sleep(random.uniform(0.5, 1.5))
    os.close(w)
    print "child: closing"
    sys.exit(0)

os.waitpid(pid, 0) # make sure the child process gets cleaned up
os.waitpid(pid2, 0) # make sure the child2 process gets cleaned up
os.waitpid(pid3, 0) # make sure the child3 process gets cleaned up


    这段代码的执行结果如下:

black@slack:~/test/pipe$ python pipe3.py
I'm going to fork now - the child will write something to a pipe, and the parent will read it back
            child2 write.
parent got: <I'm from child2, i:0>
            child write.
child3 got: <I'm from child, i:0>
            child2 write.
child3 got: <I'm from child2, i:1>
            child write.
parent got: <I'm from child, i:1>
            child2 write.
child3 got: <I'm from child2, i:2>
            child write.
parent got: <I'm from child, i:2>
            child2 write.
child3 got: <I'm from child2, i:3>
            child write.
parent got: <I'm from child, i:3>
            child2 write.
child3 got: <I'm from child2, i:4>
            child write.
parent got: <I'm from child, i:4>
            child2 write.
child3 got: <I'm from child2, i:5>
            child write.
parent got: <I'm from child, i:5>
            child2 write.
child3 got: <I'm from child2, i:6>
            child write.
parent got: <I'm from child, i:6>
            child2 write.
child3 got: <I'm from child2, i:7>
            child write.
parent got: <I'm from child, i:7>
            child2 write.
child3 got: <I'm from child2, i:8>
            child write.
parent got: <I'm from child, i:8>
            child2 write.
child3 got: <I'm from child2, i:9>
            child write.
parent got: <I'm from child, i:9>
            child2 write.
child3 got: <I'm from child2, i:10>
            child write.
parent got: <I'm from child, i:10>
child2: closing
child3: closing
child: closing
black@slack:~/test/pipe$ 


    在执行结束后,如果child与child2都结束了,而child3还没结束的话,可以在终端里手动通过ctrl+c组合键将进程终止掉。

    上面这个例子中,child与child2进程会对管道的写入端进行写入操作,parent和child3进程则会对管道的读端进行读取操作。如下图所示:


图3

    对于child与child2,如果child进程先执行的话,那么child的数据就会被先写入管道,如果child2先执行的话,那么child2的数据就会被先写入管道,先写入管道的数据,会被管道的另一端先读取出来。对于parent与child3,也是哪个进程先执行,哪个就先将管道里的数据给读取出来。

    在上面的pipe3.py脚本中,对每个进程都使用了time.sleep(random.uniform(0.5, 1.5))语句,这样可以让进程的执行更具有随机性,容易测试出管道的这种先写入与先读取的特性。此外,在脚本里使用的是os.write与os.read方法来对管道进行写入与读取的操作,并没有使用文件对象的读写方法,这是因为,文件对象的读写方法会存在一个缓存问题,缓存会影响测试结果。

    子进程之所以能够访问父进程创建的管道,是因为子进程继承了父进程的文件描述符,也就是上面提到过的拷贝了父进程的fd array。那么,如果进程A没有从进程B继承文件描述符的话,那么在正常情况下,进程A就无法访问进程B创建的管道了。例如下面这个例子:

# pipe4.py
import os, sys, time

print "the child will create a pipe, and the parent will try to access it"

r, w = os.pipe() # these are file descriptors, not file objects

pid = os.fork()
if pid:
    # we are the parent
    os.close(w) # use os.close() to close a file descriptor
    ro = os.fdopen(r) # turn r into a file object
    txt = ro.readline()
    txt = txt.rstrip('\n')
    lst = txt.split(':')
    r_fd_child = int(lst[0])
    w_fd_child = int(lst[1])
    print "parent got read fd from child: %d" % r_fd_child
    print "parent got write fd from child: %d" % w_fd_child
    wo = os.fdopen(w_fd_child, 'w')
    print "parent write to child pipe"
    wo.write("some text from parent\n")
    wo.close()
    os.waitpid(pid, 0)
else:
    # we are the child
    os.close(r)
    w = os.fdopen(w, 'w')
    r_fd_child, w_fd_child = os.pipe()
    print "child:create pipe (read fd:%d, write fd:%d)" % (r_fd_child, w_fd_child)
    print "child:pass '%d:%d' through parent pipe" % (r_fd_child, w_fd_child)
    w.write("%d:%d\n" % (r_fd_child, w_fd_child))
    w.close()
    ro = os.fdopen(r_fd_child, 'r')
    print ro.readline()
    print 'child exit'
    sys.exit(0)


    这段代码的执行结果如下:

black@slack:~/test/pipe$ python pipe4.py
the child will create a pipe, and the parent will try to access it
child:create pipe (read fd:3, write fd:5)
child:pass '3:5' through parent pipe
parent got read fd from child: 3
parent got write fd from child: 5
Traceback (most recent call last):
  File "pipe4.py", line 20, in <module>
    wo = os.fdopen(w_fd_child, 'w')
OSError: [Errno 9] Bad file descriptor
black@slack:~/test/pipe$ 


    可以看到,子进程创建的管道,父进程在正常情况下,是无法访问到的。即使把子进程中与管道相关的文件描述符传递给父进程,父进程也无法使用,因为在父进程的fd array中并没有对应的file description的pointer。

    不过,在linux系统中有一个sendmsg与recvmsg系统调用,通过这两个系统调用可以将一个进程的文件描述符发送给另一个进程,并且在另一个进程的fd array中设置好对应的pointer。在python的官方模块中找不到这两个系统调用的封装模块,好在一些第三方的模块对这两个系统调用做了封装,并且简化了发送描述符的操作,例如:python-fdsend模块,该模块的github地址为 https://github.com/fknittel/python-fdsend

    如果你要组建和安装fdsend模块的话,需要先确保你的系统中有python的开发环境,如果是ubuntu系统,可以通过安装python-dev来组建这个环境:

sudo apt-get install python-dev

    如果你的ubuntu系统中没有安装这个python-dev的话,那么下面在组建和编译fdsend模块时,就会出现fatal error: Python.h: No such file or directory的错误。作者所使用的Slackware Linux在一开始是通过源代码的方式编译安装的python,因此,python的开发环境在编译安装python时就已经安装好了。

    从github上,下载并解压fdsend:

black@slack:~$ unzip python-fdsend-master.zip 
Archive:  python-fdsend-master.zip
5990aea2c53e9fea26b4e5885fa9ad04fb8b9c1e
   creating: python-fdsend-master/
  inflating: python-fdsend-master/COPYING  
  inflating: python-fdsend-master/ChangeLog  
  inflating: python-fdsend-master/MANIFEST.in  
  inflating: python-fdsend-master/NEWS  
  inflating: python-fdsend-master/README.md  
  inflating: python-fdsend-master/TODO  
  inflating: python-fdsend-master/_fdsend.c  
   creating: python-fdsend-master/fdsend/
  inflating: python-fdsend-master/fdsend/__init__.py  
  inflating: python-fdsend-master/fdsend/tests.py  
  inflating: python-fdsend-master/setup.cfg  
  inflating: python-fdsend-master/setup.py  
black@slack:~$ ls
python-fdsend-master  python-fdsend-master.zip	test
black@slack:~$ 


    接着,进入解压的目录,并使用python setup.py build来组建和编译C源码:

black@slack:~$ cd python-fdsend-master
black@slack:~/python-fdsend-master$ ls
COPYING    MANIFEST.in	README.md  _fdsend.c  setup.cfg
ChangeLog  NEWS		TODO	   fdsend     setup.py
black@slack:~/python-fdsend-master$ python setup.py build
running build
running build_py
creating build
creating build/lib.linux-i686-2.7-pydebug
creating build/lib.linux-i686-2.7-pydebug/fdsend
copying fdsend/__init__.py -> build/lib.linux-i686-2.7-pydebug/fdsend
copying fdsend/tests.py -> build/lib.linux-i686-2.7-pydebug/fdsend
running build_ext
building '_fdsend' extension
creating build/temp.linux-i686-2.7-pydebug
gcc -pthread -fno-strict-aliasing -g -O2 -g -O0 -Wall -Wstrict-prototypes -fPIC -I/usr/local/include/python2.7 -c _fdsend.c -o build/temp.linux-i686-2.7-pydebug/_fdsend.o
gcc -pthread -shared build/temp.linux-i686-2.7-pydebug/_fdsend.o -o build/lib.linux-i686-2.7-pydebug/_fdsend.so
black@slack:~/python-fdsend-master$ ls
COPYING    MANIFEST.in	README.md  _fdsend.c  fdsend	 setup.py
ChangeLog  NEWS		TODO	   build      setup.cfg
black@slack:~/python-fdsend-master$ 


    最后通过sudo python setup.py install命令将组建好的模块安装到python的lib(库目录)中:

black@slack:~/python-fdsend-master$ sudo python setup.py install
running install
running build
running build_py
running build_ext
running install_lib
copying build/lib.linux-i686-2.7-pydebug/_fdsend.so -> /usr/local/lib/python2.7/site-packages
creating /usr/local/lib/python2.7/site-packages/fdsend
copying build/lib.linux-i686-2.7-pydebug/fdsend/__init__.py -> /usr/local/lib/python2.7/site-packages/fdsend
copying build/lib.linux-i686-2.7-pydebug/fdsend/tests.py -> /usr/local/lib/python2.7/site-packages/fdsend
byte-compiling /usr/local/lib/python2.7/site-packages/fdsend/__init__.py to __init__.pyc
byte-compiling /usr/local/lib/python2.7/site-packages/fdsend/tests.py to tests.pyc
running install_egg_info
Writing /usr/local/lib/python2.7/site-packages/fdsend-0.2.1-py2.7.egg-info
black@slack:~/python-fdsend-master$ 


    这样fdsend模块就安装好了,下面的例子中就使用fdsend来发送文件描述符:

# pipe5.py
import fdsend
import socket  # for socket.error 
import os, sys, time

print "the child will create a pipe, and the parent will try to access it"

def try_connect(sock, sock_fn, retries=30):
    """Try connecting with `sock` to `sock_fn`.  The connection is attempted
    `retries` times.
    """
    try_num = 1
    while True:
        try:
            sock.connect(sock_fn)
            return
        except socket.error:
            if try_num > retries:
                raise
            time.sleep(0.1)
            try_num += 1

pid = os.fork()
if pid:
    sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
    try_connect(sock, 'tmpsocket')
    (msg, fds) = fdsend.recvfds(sock, 1024, numfds=64)
    print "parent got fd from child: %d,%d" % fds
    sock.close()
    # Reopen the filedescriptor as a Python File-object.
    wo = os.fdopen(fds[1], 'w')
    print 'parent:write to child pipe.'
    wo.write("some text from parent\n")
    wo.close()
    os.closerange(fds[0], fds[1] + 1)
    print "parent:close"
    os.waitpid(pid, 0) # make sure the child process gets cleaned up
else:
    r_fd_child, w_fd_child = os.pipe()
    print "child:create pipe (read fd:%d, write fd:%d)" % (r_fd_child, w_fd_child)
    if os.path.exists('tmpsocket'):
        os.remove('tmpsocket')
    sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
    sock.bind('tmpsocket')
    sock.listen(1)
    client_sock, _ = sock.accept()
    sock.close()
    print "child:pass fd '%d,%d' use fdsend" % (r_fd_child, w_fd_child)
    send_fds = [r_fd_child, w_fd_child]
    fdsend.sendfds(client_sock, "here you go!", fds = send_fds)
    time.sleep(0) # use time.sleep(0) yield to whatever other thread may be ready
    ro = os.fdopen(r_fd_child, 'r')
    print 'child got it:', ro.readline()
    os.close(w_fd_child)
    print "child: closing"
    sys.exit(0)


    这段代码的执行结果如下:

black@slack:~/test/pipe$ python pipe5.py
the child will create a pipe, and the parent will try to access it
child:create pipe (read fd:3, write fd:4)
child:pass fd '3,4' use fdsend
parent got fd from child: 4,5
parent:write to child pipe.
parent:close
child got it: some text from parent

child: closing
black@slack:~/test/pipe$ 


    可以看到,通过fdsend模块,parent父进程就收到子进程的有效的文件描述符了,父进程使用这些描述符就可以访问到子进程创建的管道了。虽然parent与管道读写相关的描述符的整数值为4和5,子进程为3和4,但是它们在fd array中的pointer都是一致的,parent之所以是4和5,是因为parent中用到的socket套接字占用了3的描述符值。

    在Linux的proc目录中有每个进程打开的文件描述符的相关信息,我们对上面的pipe5.py做如下修改,让它在执行时可以暂停下来,好让我们可以在proc目录内找到文件描述符的相关信息:

# pipe6.py
import fdsend
import socket  # for socket.error 
import os, sys, time

print "the child will create a pipe, and the parent will try to access it"

def try_connect(sock, sock_fn, retries=30):
    """Try connecting with `sock` to `sock_fn`.  The connection is attempted
    `retries` times.
    """
    try_num = 1
    while True:
        try:
            sock.connect(sock_fn)
            return
        except socket.error:
            if try_num > retries:
                raise
            time.sleep(0.1)
            try_num += 1

pid = os.fork()
if pid:
    print "parent pid:", os.getpid()
    sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
    try_connect(sock, 'tmpsocket')
    (msg, fds) = fdsend.recvfds(sock, 1024, numfds=64)
    print "parent got fd from child: %d,%d" % fds
    sock.close()
    # Reopen the filedescriptor as a Python File-object.
    wo = os.fdopen(fds[1], 'w')
    print 'parent:write to child pipe.'
    wo.write("some text from parent\n")
    raw_input("parent Enter something:")
    wo.close()
    os.closerange(fds[0], fds[1] + 1)
    print "parent:close"
    os.waitpid(pid, 0) # make sure the child process gets cleaned up
else:
    r_fd_child, w_fd_child = os.pipe()
    print "child:create pipe (read fd:%d, write fd:%d)" % (r_fd_child, w_fd_child)
    if os.path.exists('tmpsocket'):
        os.remove('tmpsocket')
    sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
    sock.bind('tmpsocket')
    sock.listen(1)
    client_sock, _ = sock.accept()
    sock.close()
    print "child:pass fd '%d,%d' use fdsend" % (r_fd_child, w_fd_child)
    send_fds = [r_fd_child, w_fd_child]
    raw_input("child [pid:%d] before sendfds(Enter something):" % os.getpid())
    fdsend.sendfds(client_sock, "here you go!", fds = send_fds)
    raw_input("child after sendfds(Enter something):")
    time.sleep(0) # use time.sleep(0) yield to whatever other thread may be ready
    ro = os.fdopen(r_fd_child, 'r')
    print 'child got it:', ro.readline()
    os.close(w_fd_child)
    print "child: closing"
    sys.exit(0)


    我们先让代码执行到出现提示child [pid:2630] before sendfds(Enter something):

black@zengl:~/test/pipe$ python pipe6.py
the child will create a pipe, and the parent will try to access it
parent pid: 2629
child:create pipe (read fd:3, write fd:4)
child:pass fd '3,4' use fdsend
child [pid:2630] before sendfds(Enter something):


    接着我们在另一个终端里,输入如下指令:

black@slack:~/test$ ls -l /proc/2629/fd
total 0
lrwx------ 1 black users 64 Dec 22 18:12 0 -> /dev/pts/0
lrwx------ 1 black users 64 Dec 22 18:12 1 -> /dev/pts/0
lrwx------ 1 black users 64 Dec 22 18:12 2 -> /dev/pts/0
lrwx------ 1 black users 64 Dec 22 18:12 3 -> socket:[14277]
black@slack:~/test$ ls -l /proc/2630/fd
total 0
lrwx------ 1 black users 64 Dec 22 18:12 0 -> /dev/pts/0
lrwx------ 1 black users 64 Dec 22 18:12 1 -> /dev/pts/0
lrwx------ 1 black users 64 Dec 22 18:12 2 -> /dev/pts/0
lr-x------ 1 black users 64 Dec 22 18:12 3 -> pipe:[14278]
l-wx------ 1 black users 64 Dec 22 18:12 4 -> pipe:[14278]
lrwx------ 1 black users 64 Dec 22 18:12 6 -> socket:[14280]
black@slack:~/test$ 


    2629是parent父进程的PID,2630是子进程的PID,可以看到,在fdsend发送描述符之前,父进程是看不到子进程创建的pipe:[14278]管道的(14278是管道在内核里的编号)。接着,我们按下回车符,让前面的pipe6.py脚本执行到出现提示parent Enter something:

black@slack:~/test/pipe$ python pipe6.py
the child will create a pipe, and the parent will try to access it
parent pid: 2629
child:create pipe (read fd:3, write fd:4)
child:pass fd '3,4' use fdsend
child [pid:2630] before sendfds(Enter something):
child after sendfds(Enter something):parent got fd from child: 4,5
parent:write to child pipe.
parent Enter something:


    在执行时,child after sendfds...可能会出现在parent Enter...的后面,不过这都无关紧要,只要出现这些提示的时候,不按回车符,不让程序终止即可,我们再在另一个终端内查看两个进程的fd的情况:

black@slack:~/test$ ls -l /proc/2629/fd
total 0
lrwx------ 1 black users 64 Dec 22 18:12 0 -> /dev/pts/0
lrwx------ 1 black users 64 Dec 22 18:12 1 -> /dev/pts/0
lrwx------ 1 black users 64 Dec 22 18:12 2 -> /dev/pts/0
lr-x------ 1 black users 64 Dec 22 18:21 4 -> pipe:[14278]
l-wx------ 1 black users 64 Dec 22 18:21 5 -> pipe:[14278]
black@slack:~/test$ ls -l /proc/2630/fd
total 0
lrwx------ 1 black users 64 Dec 22 18:12 0 -> /dev/pts/0
lrwx------ 1 black users 64 Dec 22 18:12 1 -> /dev/pts/0
lrwx------ 1 black users 64 Dec 22 18:12 2 -> /dev/pts/0
lr-x------ 1 black users 64 Dec 22 18:12 3 -> pipe:[14278]
l-wx------ 1 black users 64 Dec 22 18:12 4 -> pipe:[14278]
lrwx------ 1 black users 64 Dec 22 18:12 6 -> socket:[14280]
black@slack:~/test$ 


    可以看到,在fdsend模块将子进程的描述符发送给父进程后,父进程(pid为2629)就可以看到子进程创建的pipe:[14278]了,由于之前父进程中的socket:[14277]占用了文件描述符3,因此,即便该socket被关闭了,父进程还是只能使用4来表示管道的读端,5来表示管道的写入端,要判断哪个是读端哪个是写入端,可以从读写执行的属性里看到,例如4 -> pipe:[14278]的读写执行属性是lr-x------,只有r和x,没有w,因此,就是read port(管道的读端)。

    如果知道了某个进程创建的管道号的话,我们还可以用lsof命令来查看该管道正在被哪些进程访问:

black@slack:~/test$ lsof | grep 14278
python    2629      black    4r     FIFO        0,8      0t0  14278 pipe
python    2629      black    5w     FIFO        0,8      0t0  14278 pipe
python    2630      black    3r     FIFO        0,8      0t0  14278 pipe
python    2630      black    4w     FIFO        0,8      0t0  14278 pipe
black@slack:~/test$ 


    可以看到,14278管道正在被2629与2630进程访问,在2629进程中,4是读端的文件描述符(4r),5是写入端的文件描述符(5w)。

    虽然fdsend模块可以发送文件描述符,但是,该模块目前只能在Linux系统中使用,而且使用过程也比较麻烦,还要配合socket套接字。因此,如果一个进程没有从另一个进程中继承文件描述符的话,可以使用下面要介绍的mkfifo方法,通过FIFO(named pipe 命名管道)来进行管道通信。

os模块的mkfifo方法:

    mkfifo可以在磁盘中创建一个FIFO文件。上面用os.pipe创建的管道只在内核中存在一个编号,而FIFO则有自己的文件名,可以通过文件系统访问到,因此FIFO也被称作named pipe(命名管道)。任何两个不相关的进程之间,都可以通过FIFO来进行通信。mkfifo的语法格式如下(只存在于Unix系统中):

os.mkfifo(filename [, mode=0666])

    filename表示需要创建的FIFO文件名,mode为创建的文件的读写执行的访问权限。mode会受到umask的影响,实际创建的文件的访问权限会是(mode & ~umask),有关umask的概念在之前介绍os.mkdir方法时已经讲解过了,读者可以参考之前的"Python基本的I/O操作 (二)"的文章。

    下面是一个简单的例子,该例子有两个脚本,一个用于对FIFO进行写入操作,另一个则对FIFO进行读取操作:

# sender.py

import os

if(not os.path.exists('myfifo')):
    os.mkfifo('myfifo')

print 'open FIFO for write.'
fifo = open('myfifo', 'w')
print 'FIFO connected.'
message = raw_input('Enter some message:')
fifo.write(message)
print 'the message have be sent to FIFO.'
fifo.close()


    上面的sender.py可以对创建的myfifo进行写入操作,下面的receiver.py则用于读取管道数据:

# receiver.py

import os

if(not os.path.exists('myfifo')):
    os.mkfifo('myfifo')

print 'open FIFO for read.'
fifo = open('myfifo', 'r')
print 'FIFO connected.'
for line in fifo:
    print 'Received: ' + line,
fifo.close()


    mkfifo只负责创建管道文件,在创建管道文件后,需要通过open或者os.open将管道文件打开,系统才会在内核中创建具体的管道。内核里的FIFO管道会通过FIFO文件的inode节点号来进行标识,因此,当另一个进程打开FIFO文件后,就可以通过文件的inode节点号来找到对应的内核中的管道了。

    当管道的一端被打开时,如果另一端还没有被打开的话,那么打开的那一端所对应的进程在正常情况下会处于阻塞状态,直到另一端被打开才会继续执行。因此,如果sender.py先执行的话,那么它在open打开管道的写入端时会被暂时挂起,直到receiver.py打开了管道的读端后,sender.py才会继续执行。反过来也是同理,如果receiver.py先执行的话,也会等待,直到sender.py将另一端打开才会继续执行。

    在一个终端中执行sender.py,该脚本的最终执行结果如下:

black@slack:~/test/mkfifo$ python sender.py
open FIFO for write.
FIFO connected.
Enter some message:hello world
the message have be sent to FIFO.
black@slack:~/test/mkfifo$ 


    在另一个终端里执行receiver.py,该脚本的最终执行结果如下:

black@slack:~/test/mkfifo$ python receiver.py
open FIFO for read.
FIFO connected.
Received: hello world
black@slack:~/test/mkfifo$ 


    FIFO命名管道的两端也可以同时让多个进程写入,或者让多个进程读取,例如下面两个脚本:

# sender2.py

import os,time,random

if(not os.path.exists('myfifo')):
    os.mkfifo('myfifo')

print 'open FIFO for write.'
fifo = os.open('myfifo', os.O_WRONLY)
print 'FIFO connected.'
message = 'msg from pid: ' + str(os.getpid())
for i in range(11):
    os.write(fifo, message + ' ')
    print 'the message in %d have be sent to FIFO.' % os.getpid()
    time.sleep(random.uniform(0.5, 1.5))

os.close(fifo)


# receiver2.py

import os,time,random

if(not os.path.exists('myfifo')):
    os.mkfifo('myfifo')

print 'open FIFO for read.'
fifo = os.open('myfifo', os.O_RDONLY)
print 'FIFO connected.'
for i in range(11):
    r = os.read(fifo, 1024)
    print 'r:', '<'+r.rstrip(' ')+'>'
    time.sleep(random.uniform(0.5, 1.5))
os.close(fifo)


    为了进行测试,我们需要开四个终端,前两个终端中运行receiver2.py,后两个终端中运行sender2.py,尽量以最快的速度依次运行它们,让它们能几乎同步执行:

black@slack:~/test/mkfifo$ python receiver2.py
open FIFO for read.
FIFO connected.
r: <msg from pid: 2364>
r: <msg from pid: 2365>
r: <msg from pid: 2365>
r: <msg from pid: 2365>
r: <msg from pid: 2364>
r: <msg from pid: 2364>
r: <msg from pid: 2364>
r: <msg from pid: 2364>
r: <msg from pid: 2365 msg from pid: 2364>
r: <>
r: <>
black@slack:~/test/mkfifo$ 


black@slack:~/test/mkfifo$ python receiver2.py 
open FIFO for read.
FIFO connected.
r: <msg from pid: 2365>
r: <msg from pid: 2364>
r: <msg from pid: 2364>
r: <msg from pid: 2365>
r: <msg from pid: 2364>
r: <msg from pid: 2364>
r: <msg from pid: 2365>
r: <msg from pid: 2364 msg from pid: 2365>
r: <msg from pid: 2365>
r: <msg from pid: 2365>
r: <msg from pid: 2365>
black@slack:~/test/mkfifo$ 


black@slack:~/test/mkfifo$ python sender2.py
open FIFO for write.
FIFO connected.
the message in 2364 have be sent to FIFO.
the message in 2364 have be sent to FIFO.
the message in 2364 have be sent to FIFO.
the message in 2364 have be sent to FIFO.
the message in 2364 have be sent to FIFO.
the message in 2364 have be sent to FIFO.
the message in 2364 have be sent to FIFO.
the message in 2364 have be sent to FIFO.
the message in 2364 have be sent to FIFO.
the message in 2364 have be sent to FIFO.
the message in 2364 have be sent to FIFO.
black@slack:~/test/mkfifo$ 


black@slack:~/test/mkfifo$ python sender2.py
open FIFO for write.
FIFO connected.
the message in 2365 have be sent to FIFO.
the message in 2365 have be sent to FIFO.
the message in 2365 have be sent to FIFO.
the message in 2365 have be sent to FIFO.
the message in 2365 have be sent to FIFO.
the message in 2365 have be sent to FIFO.
the message in 2365 have be sent to FIFO.
the message in 2365 have be sent to FIFO.
the message in 2365 have be sent to FIFO.
the message in 2365 have be sent to FIFO.
the message in 2365 have be sent to FIFO.
black@slack:~/test/mkfifo$ 


    两个sender2.py脚本所在的进程,分别向FIFO管道的写入端写入数据,哪个进程先执行,哪个的数据就会被先写入,先写入的数据会被管道的另一端先读取出来。另外两个receiver2.py脚本所在的进程,则分别从FIFO管道的读端读取数据,先执行的进程,会先将管道里的数据给读取出来。

    由于管道的读端只能进行读操作,管道的写入端也只能进行写入操作,因此,管道属于单向通信。如果要让两个进程之间进行双向通信的话,可以建立两个管道(一个管道对应一个传输方向),例如下面这两个脚本:

# sender3.py

import os, sys, termios

if(not os.path.exists('myfifo')):
    os.mkfifo('myfifo')
if(not os.path.exists('myfifo2')):
    os.mkfifo('myfifo2')

print 'open FIFO for write.'
wfifo = open('myfifo', 'w')
print 'write FIFO connected.'

print 'open FIFO for read.'
rfifo = open('myfifo2', 'r')
print 'read FIFO connected.'

def flush_stdin():
    termios.tcflush(sys.stdin.fileno(), termios.TCIFLUSH)

while(True):
    flush_stdin()
    message = raw_input('Enter your message> ')
    wfifo.write(message + '\n')
    wfifo.flush()
    if(message == 'bye'):
        break
    sys.stdout.write('wait for receive.')
    sys.stdout.flush()
    line = rfifo.readline()
    line = line.rstrip('\n')
    print '\rReceived:', line.ljust(70)
    if line == 'bye':
        break
wfifo.close()
rfifo.close()
flush_stdin()


# receiver3.py

import os, sys, termios

if(not os.path.exists('myfifo')):
    os.mkfifo('myfifo')
if(not os.path.exists('myfifo2')):
    os.mkfifo('myfifo2')

print 'open FIFO for read.'
rfifo = open('myfifo', 'r')
print 'read FIFO connected.'

print 'open FIFO for write.'
wfifo = open('myfifo2', 'w')
print 'write FIFO connected.'

def flush_stdin():
    termios.tcflush(sys.stdin.fileno(), termios.TCIFLUSH)

while(True):
    sys.stdout.write('wait for receive.')
    sys.stdout.flush()
    line = rfifo.readline()
    line = line.rstrip('\n')
    print '\rReceived:', line.ljust(70)
    if line == 'bye':
        break
    flush_stdin()
    message = raw_input('Enter your message> ')
    wfifo.write(message + '\n')
    wfifo.flush()
    if(message == 'bye'):
        break
rfifo.close()
wfifo.close()
flush_stdin()


    这两个脚本中,sender3.py会使用myfifo对应的管道来进行写入操作,并使用myfifo2对应的管道来进行读取操作,receiver3.py则使用myfifo进行读取操作,并使用myfifo2来进行写入操作,如下图所示:


图4

    在两个终端中分别执行sender3.py与receiver3.py,这两个脚本的最终执行结果如下:

black@slack:~/test/mkfifo$ python sender3.py
open FIFO for write.
write FIFO connected.
open FIFO for read.
read FIFO connected.
Enter your message> hello, I'm black
Received: hello, I'm zenglong           
Enter your message> How are you
Received: I'm fine, thanks              
Enter your message> what are you doing
Received: I'm writing code              
Enter your message> good job! bye!
Received: bye                           
black@slack:~/test/mkfifo$ 


black@slack:~/test/mkfifo$ python receiver3.py 
open FIFO for read.
read FIFO connected.
open FIFO for write.
write FIFO connected.
Received: hello, I'm black                                                      
Enter your message> hello, I'm zenglong
Received: How are you                                                           
Enter your message> I'm fine, thanks
Received: what are you doing                                                    
Enter your message> I'm writing code
Received: good job! bye!                                                        
Enter your message> bye
black@slack:~/test/mkfifo$ 


    os.mkfifo方法也可以通过下面要介绍的os.mknod方法来实现。

os模块的mknod方法:

    在Linux系统中主要有5种文件类型:regular file(常规文件),character special file(tty终端之类的字符设备文件),block special file(磁盘之类的块设备文件),FIFO(named pipe 命名管道)文件,以及UNIX domain socket(Unix套接字)文件。这几种文件都可以通过os模块的mknod方法来进行创建,其语法格式如下:

os.mknod(filename [, mode=0600, device])

    filename用于指定需要创建的文件名。mode用于指定创建的文件的读写执行的访问权限,mode同样会受到进程的umask屏蔽位的影响,最终生成的文件的访问权限会是(mode & ~umask),umask的概念在之前介绍os.mkdir方法时已经讲解过了。

    如果要创建的是设备文件的话,设备文件的类型可以在mode参数中进行指定(下面会介绍),那么,就需要在device参数中指定设备文件的设备ID,设备ID中包含了主次设备号,主次设备号可以标识对应的设备类型,主次设备号及设备ID的概念在上一篇文章中已经介绍过了。

    os.mknod方法最终会通过底层的mknod系统调用去执行具体的操作,可以通过man 2 mknod来查看该系统调用的详情:

black@slack:~/test/mknod$ man 2 mknod
MKNOD(2)                         Linux Programmer's Manual                        MKNOD(2)

NAME
       mknod - create a special or ordinary file

SYNOPSIS
       #include <sys/types.h>
       #include <sys/stat.h>
       #include <fcntl.h>
       #include <unistd.h>

       int mknod(const char *pathname, mode_t mode, dev_t dev);

       .............................................

DESCRIPTION
       The system call mknod() creates a file system node (file, device  special  file  or
       named pipe) named pathname, with attributes specified by mode and dev.

       The  mode argument specifies both the permissions to use and the type of node to be
       created.  It should be a combination (using bitwise OR) of one of  the  file  types
       listed below and the permissions for the new node.

       The  permissions  are modified by the process's umask in the usual way: the permis-
       sions of the created node are (mode & ~umask).

       The file type must be one of S_IFREG, S_IFCHR,  S_IFBLK,  S_IFIFO  or  S_IFSOCK  to
       specify a regular file (which will be created empty), character special file, block
       special file, FIFO (named pipe), or UNIX domain socket, respectively.   (Zero  file
       type is equivalent to type S_IFREG.)

       If  the file type is S_IFCHR or S_IFBLK then dev specifies the major and minor num-
       bers of the newly created device special file (makedev(3) may be  useful  to  build
       the value for dev); otherwise it is ignored.

       If  pathname  already exists, or is a symbolic link, this call fails with an EEXIST
       error.

       .............................................
black@slack:~/test/mknod$ 


    从man手册里,我们看到可以在mode参数中通过指定S_IFREG,S_IFBLK之类的常量,来设置需要创建的文件的具体类型,这些常量都定义在Python的stat模块中,例如下面这个例子:

# mknod.py
import os, stat

if not os.path.exists('regfile'):
    os.mknod('regfile', 0666 | stat.S_IFREG)
    print 'create regular file: regfile'
if not os.path.exists('chrdev'):
    os.mknod('chrdev', 0666 | stat.S_IFCHR, os.makedev(5, 0))
    print 'create character device file: chrdev'
if not os.path.exists('blkdev'):
    os.mknod('blkdev', 0666 | stat.S_IFBLK, os.makedev(8, 0))
    print 'create block device file: blkdev'
if not os.path.exists('myfifo'):
    os.mknod('myfifo', 0666 | stat.S_IFIFO)
    print 'create FIFO: myfifo'
if not os.path.exists('mysock'):
    os.mknod('mysock', 0666 | stat.S_IFSOCK)
    print 'create UNIX domain  socket: mysock'


    这段代码的执行结果如下:

black@slack:~/test/mknod$ sudo python mknod.py
Password:
create regular file: regfile
create character device file: chrdev
create block device file: blkdev
create FIFO: myfifo
create UNIX domain  socket: mysock
black@slack:~/test/mknod$ ls -l
total 4
brw-r--r-- 1 root  root  8, 0 Dec 23 15:25 blkdev
crw-r--r-- 1 root  root  5, 0 Dec 23 15:25 chrdev
-rw-r--r-- 1 black users  637 Dec 23 15:18 mknod.py
prw-r--r-- 1 root  root     0 Dec 23 15:25 myfifo
srw-r--r-- 1 root  root     0 Dec 23 15:25 mysock
-rw-r--r-- 1 root  root     0 Dec 23 15:25 regfile
black@slack:~/test/mknod$ 


    由于创建设备文件需要root权限,因此,在执行mknod.py时,使用了sudo命令来提权。从输出中可以看到,对于常规文件,mknod会创建一个空的文件(文件的大小为0)。此外,由于mknod也可以创建FIFO管道文件,因此,os.mkfifo('myfifo', 0666)等效于os.mknod('myfifo', 0666 | stat.S_IFIFO) 。

    对于设备文件,只要设备文件的主次设备号与系统创建的设备文件的主次设备号相同,那么你就可以使用自己创建的设备文件来执行相关的操作,例如,下面这个例子:

black@slack:~/test/mknod$ sudo chown root:tty chrdev
Password:
black@slack:~/test/mknod$ sudo chmod 0666 chrdev
black@slack:~/test/mknod$ echo 'hello black' > chrdev
hello black
black@slack:~/test/mknod$ echo 'hello black' > /dev/tty
hello black
black@slack:~/test/mknod$ 


    /dev/tty可以表示当前进程所使用的终端,因此,向该设备文件写入的字符串会在当前的终端中显示出来。由于我们创建的chrdev与/dev/tty具有相同的主次设备号(主设备号都是5,次设备号都是0),因此,它们是同一个设备类型,写入到chrdev文件中的数据,也会回显到当前的终端上。不过这里还需要将chrdev的group设置为tty,同时将chrdev的访问权限设置为0666,让普通用户可以对其进行写入操作。也就是将chrdev的owner:group,以及访问权限都设置的和/dev/tty的一致。这样,才能确保自己创建的设备文件能够实现类似的功能,如果不进行这些设置的话,就容易出现Permission denied(没有权限)的错误。

    对于块设备文件也是同理:

black@slack:~/test/mknod$ stat blkdev
  File: `blkdev'
  Size: 0         	Blocks: 0          IO Block: 4096   block special file
Device: 803h/2051d	Inode: 419270      Links: 1     Device type: 8,0
Access: (0644/brw-r--r--)  Uid: (    0/    root)   Gid: (    0/    root)
Access: 2015-12-23 15:25:03.410031962 +0800
Modify: 2015-12-23 15:25:03.410031962 +0800
Change: 2015-12-23 15:25:03.410031962 +0800
 Birth: -
black@slack:~/test/mknod$ stat /dev/sda
  File: `/dev/sda'
  Size: 0         	Blocks: 0          IO Block: 4096   block special file
Device: 5h/5d	Inode: 2709        Links: 1     Device type: 8,0
Access: (0660/brw-rw----)  Uid: (    0/    root)   Gid: (    6/    disk)
Access: 2015-12-23 12:33:33.778999843 +0800
Modify: 2015-12-23 12:33:33.455999850 +0800
Change: 2015-12-23 12:33:33.455999850 +0800
 Birth: -
black@slack:~/test/mknod$ sudo /sbin/sfdisk -l /dev/sda
Password:

Disk /dev/sda: 2294 cylinders, 255 heads, 63 sectors/track
Units = cylinders of 8225280 bytes, blocks of 1024 bytes, counting from 0

   Device Boot Start     End   #cyls    #blocks   Id  System
/dev/sda1          0+     61      62-    497983+  82  Linux swap
/dev/sda2   *     62    1044-    983-   7890593   83  Linux
/dev/sda3       1044+   2294-   1251-  10043392   83  Linux
/dev/sda4          0       -       0          0    0  Empty
black@slack:~/test/mknod$ sudo /sbin/sfdisk -l blkdev 

Disk blkdev: 2294 cylinders, 255 heads, 63 sectors/track
Units = cylinders of 8225280 bytes, blocks of 1024 bytes, counting from 0

   Device Boot Start     End   #cyls    #blocks   Id  System
  blkdev1          0+     61      62-    497983+  82  Linux swap
  blkdev2   *     62    1044-    983-   7890593   83  Linux
  blkdev3       1044+   2294-   1251-  10043392   83  Linux
  blkdev4          0       -       0          0    0  Empty
black@slack:~/test/mknod$ 


    由于blkdev与/dev/sda具有相同的主次设备号(主设备号都是8,次设备号都是0),因此,它们都可以表示第一块SCSI磁盘设备。使用sfdisk -l命令查看到的磁盘信息也是一致的,只不过分区名称不同而已,一个以/dev/sda为前缀,另一个以blkdev为前缀。

    socket套接字文件,一般是由套接字对象的bind方法自动创建,不需要手动通过mknod来创建。套接字文件也可以用于进程间的通信,而且和管道的单向通信所不同的是,它可以直接实现双向通信(不需要像管道那样每个方向都创建一个管道)。例如下面这两个脚本:

# server.py
import os, sys, socket, termios

if os.path.exists( "/tmp/test_socket"):
    os.remove( "/tmp/test_socket")
print "Opening socket and create /tmp/test_socket"
server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
server.bind("/tmp/test_socket")
print "Listening..."
server.listen(1) # max 1 connection
Done = False
while True:
    print 'wait connection...'
    connection, client_address = server.accept()
    print 'connected..'
    sys.stdout.write('wait...')
    sys.stdout.flush()
    while True:
        data = connection.recv(1024)
        if(len(data) > 0):
            print 'Received:%s' % data
            if(data == 'Done'):
                Done = True
                break
            elif(data == 'bye'):
                break
            termios.tcflush(sys.stdin.fileno(), termios.TCIFLUSH)
            message = raw_input('Enter your message> ')
            try:
                connection.send(message)
                sys.stdout.write('wait...')
                sys.stdout.flush()
            except:
                break
        else:           
            break
    print 'close connection..'
    connection.close()
    if(Done):
        print 'Client let me shutdown!'
        break


# client.py
import socket, sys, termios

clientsocket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
clientsocket.connect('/tmp/test_socket')
print 'connet to /tmp/test_socket'
while True:
    termios.tcflush(sys.stdin.fileno(), termios.TCIFLUSH)
    message = raw_input('Enter your message> ')
    try:
        clientsocket.send(message)
    except:
        pass
    if(message == 'bye'):
        print 'bye'
        break
    sys.stdout.write('wait...')
    sys.stdout.flush()
    datagram = clientsocket.recv( 1024 )
    if len(datagram) > 0:
        print 'Received:', datagram
    else:
        print 'no more data, may be server is shutdown!'


    先在一个终端中运行server.py(服务端),再在另一个终端里运行client.py(客户端),两个脚本的最终执行结果如下:

black@slack:~/test/mknod$ python server.py
Opening socket and create /tmp/test_socket
Listening...
wait connection...
connected..
wait...Received:hello, anyone there
Enter your message> yes, who are you?
wait...Received:I'm king
Enter your message> king, what can I do for you?
wait...Received:I want you to shutdown
Enter your message> ok, you can input 'Done' to let me shutdown
wait...Received:So complicated
Enter your message> sorry, the programer 'black' let me to do this
wait...Received:Done
close connection..
Client let me shutdown!
black@slack:~/test/mknod$ 


black@slack:~/test/mknod$ python client.py 
connet to /tmp/test_socket
Enter your message> hello, anyone there
wait...Received: yes, who are you?
Enter your message> I'm king
wait...Received: king, what can I do for you?
Enter your message> I want you to shutdown
wait...Received: ok, you can input 'Done' to let me shutdown
Enter your message> So complicated
wait...Received: sorry, the programer 'black' let me to do this
Enter your message> Done
wait...no more data, may be server is shutdown!
Enter your message> bye
bye
black@slack:~/test/mknod$ 


    客户端通过套接字文件,与服务端建立连接,并通过该连接与服务端进行双向通信。上面就是一问一答的简单聊天模式。服务端在使用套接字对象执行bind方法时,会自动创建/tmp/test_socket的套接字文件。

    脚本中的socket.AF_UNIX表示当前套接字主要用于本地进程间的通信,socket.SOCK_STREAM表示使用TCP协议进行通信。至于具体的套接字编程不在本篇文章的讨论范围内,这里的例子只是用于说明套接字文件的基本作用。读者可以自己找一些相关的资料来学习套接字编程,套接字还可以进行网络通信,网络通信时就不是用的套接字文件了,而是使用IP地址加端口号来进行bind,在后面的例子中就会看到。

os模块的openpty方法:

    Linux系统中主要有两种终端,一种是/dev/tty1,/dev/tty2,/dev/tty3之类的最原始的命令行终端,在Linux系统下可以通过ctrl+alt+F1切换到tty1,ctrl+alt+F2切换到tty2,等等。可以看到,tty1,tty2这些终端都处于VGA文本模式界面。当系统进入到X window的图形窗口界面后,你就看不到tty1之类的文本界面的终端了(除非用ctrl+alt+Fn来切换)。

    在图形窗口界面下,字符的显示方式发生了变化,tty1这种VGA文本模式下,字符的ASCII码是直接写入显存中的,并由硬件来将字符绘制出来。但是在图形界面下,所有的东西,包括字符都需要通过驱动程序在显存中以像素为单位进行绘制。因此,图形界面下用到的大多是像/dev/pts/0之类的伪终端(英文术语为pseudo-terminal、pseudo-tty或者是pty)。

    作者的slackware linux在启动后,最开始看到的是tty1,在tty1里登录后,通过startx启动到Xfce桌面,在该桌面上运行的Xfce Terminal会在内部启动bash,并将bash的标准输入和输出设备与pty相连,这样,Xfce Terminal就可以将用户输入的命令通过pty传递给bash,bash的执行结果也会经过pty,被Xfce给读取出来,Xfce再将这些结果通过X window提供的接口显示渲染到窗口上。Xfce Terminal本身又使用的是tty1之类的终端作为标准的输入和输出设备的,因此,Xfce Terminal的警告和错误信息会显示到ttyn(ttyn表示tty1、tty2等)上,需要通过ctrl+alt+Fn(Fn表示F1,F2等)来切换查看。如下图所示:


图5

    从图中可以看到,PTY有一个Master端,还有一个Slave端。Slave端通常与需要执行标准输入输出操作的被控端进程(例如bash)相连,而Master则通常与发送和接受数据的控制端进程相连。写入Master端的数据(例如图中的command命令),可以从Slave端以stdin标准输入的方式读取出来。反过来,以stdout标准输出方式写入Slave端的数据(例如图中的result执行结果),也可以由控制端进程从Master端给读取出来。

    除了Xface Terminal会用到PTY外,还有很多其他的应用也会用到PTY,例如telnetd这种远程登录的daemon进程。我们下面的例子中就会用Python写一个简单的telnetd,然后再写一个telnet来远程登录这个daemon进程,并远程执行一些简单的命令。在写代码之前,我们需要先了解telnetd的基本原理,telnetd其实也是通过PTY与bash通信,从而执行命令,并将命令的执行结果通过网络返回给远程用户的。如下图所示:


图6

    根据这个原理图,我们先写一个telnetd.py脚本:

# telnetd.py
import os, subprocess, select, time, socket, sys, signal, atexit
import struct
from sys import platform as _platform

if _platform == "linux" or _platform == "linux2" or _platform == "darwin":
    pass
else:
    sys.exit('please run it in Linux or Mac OS X')

def prepare():
    os.setsid() # start a new detached session

def exit_proc():
    try:
        os.close(master_fd)
        os.close(slave_fd)
        bash.terminate()
        bash.kill()
        bash.wait()
        print ' kill bash and exit!'
    except:
        pass

def kill_bash_signal(signum, frame):
    exit_proc()
    sys.exit(1)

def kill_bash_atexit():
    exit_proc()
    sys.exit(1)

def write_all(masterPTY, data):
    """Successively write all of data into a file-descriptor."""
    while data:
        chars_written = os.write(masterPTY, data)
        data = data[chars_written:]
    return data

def read_all(masterPTY):
    data = ''
    count = 0
    while(True):
        r,w,x = select.select([masterPTY], [], [], 0)
        if not r:
            if(count > 5): # five chance
                print 'read out.'
                break
            count += 1
            time.sleep(0.1) # no input, sleep a moment
        else:
            data += os.read(masterPTY, 1024) # there is some input
            print 'sleeping count: %d, read data size: %d' % (count, len(data))
            count = 0
    lst = data.split('\n')
    try:
        if(len(lst) > 2):
            lst.pop(0)
    except:
        pass
    data = '\n'.join(lst)
    return data

signal.signal(signal.SIGTERM, kill_bash_signal)
atexit.register(kill_bash_atexit)

server_address = ('0.0.0.0', 8098)
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind(server_address)
server.listen(1) # max 1 connection
print "telnetd listening on %s port %s" % server_address

(master_fd, slave_fd) = os.openpty()
bash = subprocess.Popen(["/bin/bash", "-i"], \
            stdin=slave_fd, \
            stdout=slave_fd, \
            stderr=slave_fd, \
            preexec_fn=prepare)

def send_msg(sock, msg):
    # Prefix each message with a 4-byte length (network byte order)
    msg = struct.pack('>I', len(msg)) + msg
    sock.sendall(msg)

Done = False
while True:
    print 'wait connection...'
    connection, client_address = server.accept()
    print 'connection from %s:%s' % client_address
    sys.stdout.write('wait...')
    sys.stdout.flush()
    while True:
        data = connection.recv(1024)
        if(len(data) > 0):
            print 'Received:%s' % data
            if(data == 'Done'):
                Done = True
                break
            elif(data == 'bye'):
                break
            write_all(master_fd, data.rstrip('\n') + '\n')
            data = read_all(master_fd)
            try:
                send_msg(connection, data)
                sys.stdout.write('send data and wait...')
                sys.stdout.flush()
            except:
                break
        else:
            break
    print 'close connection..'
    connection.close()
    if(Done):
        print 'Client let me shutdown!'
        break

exit_proc()


    telnetd.py脚本中,会先创建一个server套接字对象,创建该对象时用到的socket.AF_INET表示server是用于网络通信的。在进行网络通信时,套接字对象的bind方法就需要指定一个IP地址和一个端口号,这里用0.0.0.0的IP地址就可以让外部网络访问进来,8098是我自定义的telnetd的监听端口(你可以根据需要,修改这个端口)。

    接着,脚本中使用os.openpty方法打开了一个PTY,并且将PTY的master与slave端的文件描述符以元组的形式作为结果返回。然后,通过subprocess.Popen方法创建子进程并在子进程中运行/bin/bash,还将bash的stdin(标准输入),stdout(标准输出)以及stderr(标准的错误输出)都设置到PTY的slave端。这样,telnetd就可以通过PTY的master端将命令传递给bash,并且从bash那获取输出结果了。得到的结果会通过套接字对象,发送给连接进来的远程客户端。

    下面是telnet.py(远程客户端)的代码:

# telnet.py
import socket, sys, os, re
import struct

if os.name != 'nt':
    import termios

if(len(sys.argv) != 2):
    sys.exit('usage: python '+ sys.argv[0] + ' <ip:port>')
lst = sys.argv[1].split(':')
if(len(lst) != 2):
    sys.exit('usage: python '+ sys.argv[0] + ' <ip:port>')

ip_address = re.findall( r'[0-9]+(?:\.[0-9]+){3}', lst[0])
if(len(ip_address) == 0):
    sys.exit('please input a valid ip address')
ip_address = ip_address[0]

port = lst[1]
if(port == '' or (not port.isdigit())):
    sys.exit('please input a valid port')
port = int(port)

clientsocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_address = (ip_address, port)
clientsocket.connect(server_address)
print 'connet to %s:%s' % server_address

def recv_msg(sock):
    # Read message length and unpack it into an integer
    raw_msglen = recvall(sock, 4)
    if not raw_msglen:
        return None
    msglen = struct.unpack('>I', raw_msglen)[0]
    # Read the message data
    return recvall(sock, msglen)

def recvall(sock, n):
    # Helper function to recv n bytes or return None if EOF is hit
    data = ''
    while len(data) < n:
        packet = sock.recv(n - len(data))
        if not packet:
            return None
        data += packet
    return data

while True:
    if os.name != 'nt':
        termios.tcflush(sys.stdin.fileno(), termios.TCIFLUSH)
    message = raw_input('\ntelnet> ')
    if(len(message) <= 0):
        continue
    try:
        clientsocket.send(message)
    except:
        pass
    if(message == 'bye'):
        print 'bye'
        break
    sys.stdout.write('wait...')
    sys.stdout.flush()
    datagram = recv_msg(clientsocket)
    if datagram and len(datagram) > 0:
        print 'Received data:\n', \
            '********************************\n', \
            datagram, \
            '\n********************************'
    else:
        print 'no more data, may be server is shutdown!'


    我们可以先在Linux或者Mac OS X系统中运行telnetd.py守护进程,然后在windows、Linux或者Mac OS X系统中执行telnet.py进行远程连接。

    下面是Linux系统中telnetd.py(服务端)的最终执行结果:

black@zengl:~/test/openpty$ python telnetd.py
telnetd listening on 0.0.0.0 port 8098
wait connection...
connection from 192.168.1.104:56194
wait...Received:pwd
sleeping count: 0, read data size: 28
sleeping count: 1, read data size: 87
read out.
send data and wait...Received:uname -a
sleeping count: 1, read data size: 175
read out.
send data and wait...Received:ls -l
sleeping count: 1, read data size: 332
read out.
send data and wait...Received:cat test.txt
sleeping count: 1, read data size: 80
read out.
send data and wait...Received:Done
close connection..
Client let me shutdown!
 kill bash and exit!
black@zengl:~/test/openpty$ 


    下面是windows系统中telnet.py(客户端)的执行情况:

G:\Python27\mytest>..\python.exe telnet.py 192.168.1.107:8098
connet to 192.168.1.107:8098

telnet> pwd
wait...Received data:
********************************
/home/black/test/openpty
black@slack:~/test/openpty$
********************************

telnet> uname -a
wait...Received data:
********************************
Linux black 2.6.37.6-smp #2 SMP Sat Apr 9 23:39:07 CDT 2011 i686 Intel(R) Core(TM) i3 CPU       M 370  @ 2.40GHz GenuineIntel GNU/Linux
black@slack:~/test/openpty$
********************************

telnet> ls -l
wait...Received data:
********************************
total 20
-rw-r--r-- 1 root  root  1741 Dec 16 20:46 copy of telnet.py
-rw-r--r-- 1 root  root  2674 Dec 16 20:34 copy of telnetd.py
-rw-r--r-- 1 black users 1753 Dec 24 17:22 telnet.py
-rw-r--r-- 1 root  root  2709 Dec 24 17:22 telnetd.py
-rw-r--r-- 1 root  root    37 Dec 24 18:16 test.txt
black@slack:~/test/openpty$
********************************

telnet> cat test.txt
wait...Received data:
********************************
hello world, welcome to black world!
black@slack:~/test/openpty$
********************************

telnet> Done
wait...no more data, may be server is shutdown!

telnet> bye
bye

G:\Python27\mytest>


    虽然telnetd.py有异常处理的代码,可以在发生严重错误时,将bash子进程给终止掉。但是如果你使用python -mpdb来调试这个脚本的话,并且在调试过程中发生了异常的话,那么启动的bash子进程就有可能不会被终止掉,这个时候你就必须手动通过kill -9命令强行将bash子进程给终止掉。如果不终止bash子进程的话,下一次执行telnetd.py时就会出现类似socket.error: [Errno 98] Address already in use的错误。这是因为,bash继承了telnetd.py父进程所创建的socket套接字的文件描述符,因此,如果bash不终止的话,telnetd所创建的套接字就没办法被释放,套接字所绑定的IP地址和端口号就会一直处于占用状态。

    telnetd.py只能执行一些简单的命令,像vim那种编辑器指令就没办法执行,而且在从PTY的master端读取bash的输出结果时,有一个超时计数器(默认值为5),如果在master端等待超过了0.5秒还没有检测到输出结果的话,就会停止读取操作。因此,对于那些要执行老半天才能产生输出结果的命令,它们的结果就无法及时获取到,只有等到下一个命令执行时,和下一个命令的输出结果一起获取出来。

    如果在telnet连接过程中出现No route to host的错误的话,可能是防火墙的问题,也可能是路由延迟问题,如果只是延迟或丢包造成的,再重新连接即可。

    对于作者来说,目前这两个脚本的最大作用在于:作者可以在一台电脑上运行Linux系统,并且将该系统中进行测试的代码通过telnet,将字符串信息传递给另一台电脑上运行的Mac OS X,这样,我就不需要为了测试简单的脚本代码,而拷贝文件了。我们可以在telnet.py连接时,使用cat命令显示文件的内容,或者用echo命令来传递简单的字符串信息。

    我们已经在前面的telnetd.py脚本中,看到了os.openpty方法的作用了,该方法的语法格式如下(只存在于部分Unix系统中,如Linux,Mac OS X,FreeBSD等):

os.openpty() -> (master_fd, slave_fd)

    它会从系统中,打开一个pty,并且将PTY的master端的文件描述符(master_fd)与slave端的文件描述符(slave_fd),以元组的形式作为结果返回。如果你想查看你的程序到底打开了哪些和pty相关的文件描述符的话,在Linux系统中,可以在进程的/proc/<pid>/fd目录里进行查看:

black@slack:~/test/openpty$ ps j 
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
 ....................................................
 2320  2461  2461  2304 pts/2     2461 S+    1001   0:00 python telnetd.py
 2461  2462  2462  2462 pts/3     2462 Ss+   1001   0:00 /bin/bash -i
 ....................................................
black@slack:~/test/openpty$ ls -l /proc/2461/fd   
total 0
lrwx------ 1 black users 64 Dec 24 18:55 0 -> /dev/pts/2
lrwx------ 1 black users 64 Dec 24 18:55 1 -> /dev/pts/2
lrwx------ 1 black users 64 Dec 24 18:55 2 -> /dev/pts/2
lrwx------ 1 black users 64 Dec 24 18:55 3 -> socket:[13442]
lrwx------ 1 black users 64 Dec 24 18:55 4 -> /dev/ptmx
lrwx------ 1 black users 64 Dec 24 18:55 5 -> /dev/pts/3
black@slack:~/test/openpty$ ls -l /proc/2462/fd
total 0
lrwx------ 1 black users 64 Dec 24 18:54 0 -> /dev/pts/3
lrwx------ 1 black users 64 Dec 24 18:56 1 -> /dev/pts/3
lrwx------ 1 black users 64 Dec 24 18:55 2 -> /dev/pts/3
lrwx------ 1 black users 64 Dec 24 18:56 255 -> /dev/pts/3
lrwx------ 1 black users 64 Dec 24 18:56 3 -> socket:[13442]
lrwx------ 1 black users 64 Dec 24 18:56 4 -> /dev/ptmx
black@slack:~/test/openpty$ 


    可以看到python telnetd.py所在的进程(PID = 2461),打开了4和5两个文件描述符作为PTY的主从端。其中,4是master端,5是slave端,openpty方法可以通过打开/dev/ptmx文件来实现,当打开/dev/ptmx文件时,会返回master端的文件描述符,同时在/dev/pts目录中分配一个关联的设备文件作为slave端,例如上面的5 -> /dev/pts/3。

    对于bash子进程(PID = 2462),它的0(标准输入),1(标准输出),2(标准错误输出)的文件描述符都指向了/dev/pts/3,也就是父进程分配到的PTY的slave端。同时因为继承关系,bash也继承到了父进程的3 -> socket:[13442]以及4 -> /dev/ptmx,不过继承过来的这两个文件描述符对于bash来说没什么用。

os模块的popen方法:

    os.popen方法的语法格式如下:

os.popen(command [, mode='r' [, buffering]]) -> pipe

    该方法会新建一个子进程,并在子进程中执行command命令。它还会在当前进程与command子进程之间建立一个pipe管道,你可以通过该管道将command执行的结果读取出来,也可以向管道中写入数据,写入的数据将作为command命令的输入参数。该方法返回的是一个与pipe管道相关的文件对象,通过调用文件对象的读写方法,就可以对管道进行读写操作。

    mode参数用于指定返回的文件对象的读写模式,当为'r'时,可以进行读操作,当为'w'时,可以进行写入操作。此外,mode还可以指定一个额外的'e'字符,表示该文件对象里的文件描述符是否设置close-on-exec的标志,如果设置了该标志的话,那么当进程中执行execl之类的系统调用时,该文件描述符就会被自动关闭掉,该标志主要用于防止子进程在切换到其他程式的镜像时,把父进程中不相关的文件描述符(或者是父进程不愿意其他程式访问到的文件描述符)也继承过去。

    buffering参数用于指定返回的文件对象的缓存机制,该参数的含义,与之前文章中介绍过的open内建函数里的buffering参数的含义是一致的。

    在Linux系统中,os.popen方法最终会通过popen的C库函数去执行具体的操作,因此,我们可以通过man popen来查看其详情:

black@slack:~/test$ man popen
POPEN(3)                   Linux Programmer's Manual                  POPEN(3)

NAME
       popen, pclose - pipe stream to or from a process

SYNOPSIS
       #include <stdio.h>

       FILE *popen(const char *command, const char *type);

       int pclose(FILE *stream);

       .............................................

DESCRIPTION
       The popen() function opens a process by creating a pipe,  forking,  and
       invoking  the shell.  Since a pipe is by definition unidirectional, the
       type argument may specify  only  reading  or  writing,  not  both;  the
       resulting stream is correspondingly read-only or write-only.

       The  command argument is a pointer to a null-terminated string contain-
       ing a shell command line.  This command is passed to /bin/sh using  the
       -c  flag;  interpretation, if any, is performed by the shell.  The type
       argument is a pointer to a null-terminated string  which  must  contain
       either the letter 'r' for reading or the letter 'w' for writing.  Since
       glibc 2.9, this argument can additionally include the letter 'e', which
       causes  the close-on-exec flag (FD_CLOEXEC) to be set on the underlying
       file descriptor; see the description of the O_CLOEXEC flag  in  open(2)
       for reasons why this may be useful.

       .............................................
black@slack:~/test$ 


    可以看到,popen会为子进程创建一个pipe管道,然后调用shell,并将command命令传递给shell去执行。由于pipe是单向的,因此,返回的文件对象要么是read-only(只读的),要么是write-only(只写的)。

    还可以看到,只有在glibc大于等于2.9的系统环境下,os.popen方法的mode参数中才能指定额外的字符'e' 。我们可以通过ldd --version命令来查看glibc的版本号:

black@slack:~/test$ ldd --version
ldd (GNU libc) 2.13
Copyright (C) 2011 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
Written by Roland McGrath and Ulrich Drepper.
black@slack:~/test$ 


    我的系统中glibc是2.13的版本,因此,可以在mode参数中指定字符'e' 。

    下面是os.popen方法的一个简单的例子:

# popen.py
import os

p = os.popen('ps', 'r')
for line in p.readlines():
    print line


    上面这个脚本会去执行ps命令,并将该命令的执行结果读取和显示出来:

black@slack:~/test/popen$ python popen.py
  PID TTY          TIME CMD

 2125 pts/1    00:00:00 bash

 2346 pts/1    00:00:00 python

 2347 pts/1    00:00:00 ps

black@slack:~/test/popen$ 


    对于那些需要输入参数的命令,可以将mode设置为'w',然后将参数写入管道,例如下面这个脚本:

# popen_write.py

import os

p = os.popen('less', 'w')
f = open('popen.py')
p.write(f.read())


    该脚本将popen.py文件的内容,通过管道传递给less(作为该命令的输入参数),这样就可以在less中查看指定文件的内容了:

black@slack:~/test/popen$ python popen_write.py
# popen.py
import os

p = os.popen('ps', 'r')
for line in p.readlines():
        print line

lines 1-7/7 (END)


    要测试mode参数中额外的字符'e'的作用的话,我们需要写两个脚本,一个是父进程所在的主程式,另一个是子进程需要通过execl系统调用来执行的子程式:

# popen_e.py
import os, sys

if len(sys.argv) != 2:
    sys.exit('usage: python ' + sys.argv[0] + ' <r|re>')

if sys.argv[1] == 'r':
    p = os.popen('ps', 'r')
elif sys.argv[1] == 're':
    p = os.popen('ps', 're')
else:
    sys.exit('usage: python ' + sys.argv[0] + ' <r|re>')

pid = os.fork()
if pid > 0:
    os.waitpid(pid, 0)
elif pid == 0:
    os.execl(sys.executable, 'python', 'child.py', str(p.fileno()))
    print "I'll never be reached!"


# child.py

import os, sys

print "I'm in child process!\n"

fd = int(sys.argv[1])
f = os.fdopen(fd)
for line in f.readlines():
    print line


    执行结果如下:

black@slack:~/test/popen$ python popen_e.py r
I'm in child process!

  PID TTY          TIME CMD

 2125 pts/1    00:00:00 bash

 2441 pts/1    00:00:00 python

 2442 pts/1    00:00:00 ps

 2443 pts/1    00:00:00 python

black@slack:~/test/popen$ python popen_e.py re
I'm in child process!

Traceback (most recent call last):
  File "child.py", line 8, in <module>
    f = os.fdopen(fd)
OSError: [Errno 9] Bad file descriptor
black@slack:~/test/popen$ 


    可以看到,在没有字符'e'的情况下,子进程通过execl系统调用切换到child.py程式所在的代码镜像后,依然可以访问到父进程在popen_e.py程式中打开的管道,并通过该管道将ps命令的执行结果给读取了出来。在存在字符'e'的情况下,子进程在执行execl系统调用时,父进程打开的管道描述符就会被自动关闭掉,因此,child.py再想使用该文件描述符时,就会抛出Bad file descriptor的错误了。

os模块的readlink方法:

    os.readlink方法可以将符号链接所指向的目标文件的路径信息,以字符串的形式作为结果返回。语法格式如下:

os.readlink(symlink_path) -> target_path

    symlink_path为符号链接的路径,返回的target_path是一个字符串,该字符串包含了目标文件的路径信息。下面是一段简单的测试代码:

# readlink.py
import os

result = os.readlink('symlink')
print 'symlink point to', result
print 'symlink point to absolute path:', os.path.abspath(result)


    脚本中使用了os.path.abspath来获取目标文件的绝对路径。这段代码的执行结果如下:

black@slack:~/test/readlink$ touch test.txt
black@slack:~/test/readlink$ ln -s test.txt symlink
black@slack:~/test/readlink$ ls -l
total 4
-rw-r--r-- 1 root  root  142 Dec 25 15:05 readlink.py
lrwxrwxrwx 1 black users   8 Dec 25 15:05 symlink -> test.txt
-rw-r--r-- 1 black users   0 Dec 25 15:05 test.txt
black@slack:~/test/readlink$ python readlink.py 
symlink point to test.txt
symlink point to absolute path: /home/black/test/readlink/test.txt
black@slack:~/test/readlink$ 


    从Python 2.6版本开始,如果readlink的参数是Unicode对象的话,那么返回的结果也会是Unicode对象:

black@slack:~/test/readlink$ python
Python 2.7.8 (default, Feb 20 2015, 12:54:46) 
[GCC 4.5.2] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import os
>>> os.readlink(u'symlink')
u'test.txt'
>>> type(os.readlink(u'symlink'))
<type 'unicode'>
>>> quit()
black@slack:~/test/readlink$ 


os模块的stat_float_times方法:

    之前的文章中,我们介绍过os.stat方法,该方法会返回文件的相关信息,具体的信息字段定义在内部的stat_result_fields的C结构中(该结构位于Python源代码的Modules/posixmodule.c文件里):

PyDoc_STRVAR(stat_result__doc__,
"stat_result: Result from stat or lstat.\n\n\
This object may be accessed either as a tuple of\n\
  (mode, ino, dev, nlink, uid, gid, size, atime, mtime, ctime)\n\
or via the attributes st_mode, st_ino, st_dev, st_nlink, st_uid, and so on.\n\
\n\
Posix/windows: If your platform supports st_blksize, st_blocks, st_rdev,\n\
or st_flags, they are available as attributes only.\n\
\n\
See os.stat for more information.");

static PyStructSequence_Field stat_result_fields[] = {
    {"st_mode",    "protection bits"},
    {"st_ino",     "inode"},
    {"st_dev",     "device"},
    {"st_nlink",   "number of hard links"},
    {"st_uid",     "user ID of owner"},
    {"st_gid",     "group ID of owner"},
    {"st_size",    "total size, in bytes"},
    /* The NULL is replaced with PyStructSequence_UnnamedField later. */
    {NULL,   "integer time of last access"},
    {NULL,   "integer time of last modification"},
    {NULL,   "integer time of last change"},
    {"st_atime",   "time of last access"},
    {"st_mtime",   "time of last modification"},
    {"st_ctime",   "time of last change"},
#ifdef HAVE_STRUCT_STAT_ST_BLKSIZE
    {"st_blksize", "blocksize for filesystem I/O"},
#endif
#ifdef HAVE_STRUCT_STAT_ST_BLOCKS
    {"st_blocks",  "number of blocks allocated"},
#endif
#ifdef HAVE_STRUCT_STAT_ST_RDEV
    {"st_rdev",    "device type (if inode device)"},
#endif
#ifdef HAVE_STRUCT_STAT_ST_FLAGS
    {"st_flags",   "user defined flags for file"},
#endif
#ifdef HAVE_STRUCT_STAT_ST_GEN
    {"st_gen",    "generation number"},
#endif
#ifdef HAVE_STRUCT_STAT_ST_BIRTHTIME
    {"st_birthtime",   "time of creation"},
#endif
    {0}
};


    在stat方法返回的stat_result对象中,只有前7个成员既可以用属性成员的方式来访问,又可以用元组索引的方式来访问,如:stat_result.st_mode等效于stat_result[0],stat_result.st_ino等效于stat_result[1],等等。

    前10个成员中的最后三个NULL成员分别存储的是atime,mtime及ctime的整数值,由于字段名为NULL,也就是Unnamed Field(未命名字段),因此这部分只能以索引的方式来访问。例如:stat_result[7]对应atime的整数形式的时间戳,stat_result[8]对应mtime的整数形式的时间戳,stat_result[9]对应ctime的整数形式的时间戳。

    超过10个成员的部分,也就是st_atime字段及其之后的所有字段,都只能以属性成员的方式去访问。因为stat_result对象内部的元组大小只设置到了10。浮点格式的时间戳也只会存储在st_atime,st_mtime及st_ctime字段中。可以通过stat_result.st_atime来访问atime的浮点格式的时间戳,stat_result.st_mtime来访问mtime的浮点格式的时间戳,以及使用stat_result.st_ctime来访问ctime的浮点格式的时间戳。

    当然,如果内部的_stat_float_times这个C全部变量是0的话,那么,stat_result中所有的时间戳都会是整数值,可以参考fill_time这个C函数,该C函数是用于填充stat_result对象中的时间戳信息的,也定义在Python源代码的Modules/posixmodule.c文件里:

static void
fill_time(PyObject *v, int index, time_t sec, unsigned long nsec)
{
    PyObject *fval,*ival;
#if SIZEOF_TIME_T > SIZEOF_LONG
    ival = PyLong_FromLongLong((PY_LONG_LONG)sec);
#else
    ival = PyInt_FromLong((long)sec);
#endif
    if (!ival)
        return;
    if (_stat_float_times) {
        fval = PyFloat_FromDouble(sec + 1e-9*nsec);
    } else {
        fval = ival;
        Py_INCREF(fval);
    }
    PyStructSequence_SET_ITEM(v, index, ival);
    PyStructSequence_SET_ITEM(v, index+3, fval);
}


    ival对应为时间戳的整数值,fval在_stat_float_times这个C全局变量不为0时,会将nsec(纳秒)也统计进去,以得到时间戳的浮点数。如果_stat_float_times为0的话,就会将ival的整数值,直接赋值给fval,这样得到的结果中就只能看到整数形式的时间戳了。

    上面的PyStructSequence_SET_ITEM(v, index, ival);会将前面提到的NULL(Unamed Field 未命名字段)的值设置为ival,因此stat_result[7],stat_result[8],stat_result[9]这三个未命名字段的时间戳始终会是整数类型。

    而PyStructSequence_SET_ITEM(v, index+3, fval);则会将前面提到的st_atime,st_mtime或者是st_ctime字段的值设置为fval,因此,这三个字段的值在_stat_float_times不为0时,会是浮点数,在_stat_float_times为0时,就会是整数。

    _stat_float_times这个C全局变量可以通过os.stat_float_times方法进行设置,或者通过该方法来返回当前的值。os.stat_float_times方法会调用的底层C函数是stat_float_times,该函数也定义在Modules/posixmodule.c文件中:

/* If true, st_?time is float. */
static int _stat_float_times = 1;

PyDoc_STRVAR(stat_float_times__doc__,
"stat_float_times([newval]) -> oldval\n\n\
Determine whether os.[lf]stat represents time stamps as float objects.\n\
If newval is True, future calls to stat() return floats, if it is False,\n\
future calls return ints. \n\
If newval is omitted, return the current setting.\n");

static PyObject*
stat_float_times(PyObject* self, PyObject *args)
{
    int newval = -1;
    if (!PyArg_ParseTuple(args, "|i:stat_float_times", &newval))
        return NULL;
    if (newval == -1)
        /* Return old value */
        return PyBool_FromLong(_stat_float_times);
    _stat_float_times = newval;
    Py_INCREF(Py_None);
    return Py_None;
}


    当没提供任何参数时,会将当前的_stat_float_times的值转为Bool对象,并将其作为结果返回。如果提供了参数,那么就会使用你提供的参数的整数值,作为_stat_float_times变量的新的值。从前面的fill_time函数中可以看到,只要你提供的参数不为0,那么os.stat方法返回的stat_result对象中的st_atime,st_mtime及st_ctime字段的值就会是浮点数,如果你提供的参数等于0,那么就只能得到整数形式的时间戳。如果你提供的参数是Bool对象的话,那么True就会转化为整数值1,False会转换为整数值0 。此外,_stat_float_times这个C全局变量的初始值为1,也就是默认情况下,我们可以得到浮点格式的时间戳。

    下面是一个简单的测试脚本:

# stat_float_times.py
import os

def conv_to_string(x):
    if isinstance(x, float):
        return '%f %s' % (x, type(x))
    elif isinstance(x, int):
        return '%d %s' % (x, type(x))
    else:
        return ''

print 'current stat_float_times:', os.stat_float_times()
stat_result = os.stat('stat_float_times.py')
print 'stat_result[7]:', stat_result[7]
print 'stat_result.st_atime:', conv_to_string(stat_result.st_atime)
print 'stat_result[8]:', stat_result[8]
print 'stat_result.st_mtime:', conv_to_string(stat_result.st_mtime)
print 'stat_result[9]:', stat_result[9]
print 'stat_result.st_ctime:', conv_to_string(stat_result.st_ctime)

os.stat_float_times(False)
print '\nnow change stat_float_times to False.\n'

stat_result = os.stat('stat_float_times.py')
print 'stat_result[7]:', stat_result[7]
print 'stat_result.st_atime:', conv_to_string(stat_result.st_atime)
print 'stat_result[8]:', stat_result[8]
print 'stat_result.st_mtime:', conv_to_string(stat_result.st_mtime)
print 'stat_result[9]:', stat_result[9]
print 'stat_result.st_ctime:', conv_to_string(stat_result.st_ctime)


    这个脚本的执行结果如下:

black@slack:~/test/statft$ python stat_float_times.py 
current stat_float_times: True
stat_result[7]: 1451038794
stat_result.st_atime: 1451038794.447790 <type 'float'>
stat_result[8]: 1451038788
stat_result.st_mtime: 1451038788.245790 <type 'float'>
stat_result[9]: 1451038788
stat_result.st_ctime: 1451038788.255790 <type 'float'>

now change stat_float_times to False.

stat_result[7]: 1451038794
stat_result.st_atime: 1451038794 <type 'int'>
stat_result[8]: 1451038788
stat_result.st_mtime: 1451038788 <type 'int'>
stat_result[9]: 1451038788
stat_result.st_ctime: 1451038788 <type 'int'>
black@slack:~/test/statft$ 


    在stat_float_times为True时,得到的st_atime,st_mtime及st_ctime会是float浮点数。当stat_float_times被设置为False后,得到的时间戳就都是int类型的整数值了。

os模块的tcgetpgrp、tcsetpgrp及ttyname方法:

    要理解tcgetpgrp与tcsetpgrp方法的含义,首先就需要清楚进程组及会话的概念。下面会先对进程组和会话的概念进行讲解,再介绍这些方法的使用。

    在Linux系统中,为了方便job control(作业控制),就有了session(会话)和process group(进程组)。一个会话通常与一个tty终端或者pts伪终端相关联(终端与伪终端在前面介绍openpty方法时讲解过),一个会话中可以包含多个进程组,这些进程组将共用会话关联的终端,而一个进程组中则可以包含多个进程,进程组中的这些进程大多是父子关系的进程。当然会话也可以不与任何终端相关联,例如一些daemon守护进程,它们不需要与终端进行交互,那么它们通常就会新建一个不与任何终端相关联的会话,这样,在该会话中的daemon进程就不会受到终端的干扰和影响了。

    会话包含的进程组中,有一个是foreground process group(前台进程组),前台进程组可以接受到终端发送的控制信号。例如:当在终端上按下Ctrl + C组合键时,终端就会向前台进程组里的所有进程发送SIGINT信号,从而将该进程组里的所有进程都kill(结束掉)。一般情况下,也是前台进程组负责与终端进行交互,也就是从终端接受输入数据,以及输出数据到终端。

    除了前台进程组外,会话中的其他进程组都是background process group(后台进程组),后台进程组在正常情况下是不与终端进行交互的,当这些后台进程组里的进程试图从会话关联的终端中读取数据时,就会收到SIGTTIN信号,在该信号的作用下,进程就会被暂时挂起(暂停执行)。严格来说,后台进程组里的进程也不应该向终端输出数据,但是,默认情况下,它们还是可以输出数据到终端里的,除非终端使用stty tostop命令来严格禁止后台进程产生输出数据,在stty tostop作用下,后台进程向终端输出数据时,就会收到SIGTTOU信号,该信号的默认动作也是将进程暂时挂起。

    当会话中的前台进程组结束后,其中一个后台进程组就会恢复成为新的前台进程组。例如,当在bash中执行某个some_app应用时,some_app会新建一个进程组,并让该进程组成为前台进程组,此时some_app就可以与终端进行交互了,bash所在进程组则从前台跑到了后台,当some_app结束后,bash所在的进程组就会从后台恢复到前台,从而可以继续接受用户输入的其他命令了。

    在会话中第一个运行的进程,将成为该会话的leader,并且该leader进程的PID将成为会话的session ID(会话ID)。同样的,进程组中的第一个进程将成为该进程组的leader,该leader的PID也将成为进程组的process group ID(进程组ID)。

    我们以前面介绍过的pipe6.py脚本为例,当在一个终端里运行pipe6.py时,我们在另一个终端中通过ps ajf命令,来查看与python pipe6.py进程相关的进程组和会话的情况:

black@slack:~/test/pipe$ python pipe6.py
the child will create a pipe, and the parent will try to access it
parent pid: 2114
child:create pipe (read fd:3, write fd:4)
child:pass fd '3,4' use fdsend
child [pid:2115] before sendfds(Enter something):


black@slack:~/test$ ps ajf
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
 ...................................................
 2088  2090  2090  2090 pts/0     2114 Ss       0   0:00  \_ bash
 2090  2092  2092  2090 pts/0     2114 S        0   0:00  |   \_ -su
 2092  2107  2107  2090 pts/0     2114 S     1001   0:00  |       \_ bash
 2107  2114  2114  2090 pts/0     2114 S+    1001   0:00  |           \_ python pipe6.py
 2114  2115  2114  2090 pts/0     2114 S+    1001   0:00  |               \_ python pipe6.py
 ...................................................


    由于我一开始登录slackware系统时,使用的是root用户(uid = 0),之后才用su命令降权到black用户的(uid = 1001),因此,我的ps命令输出的结果中就多了su命令,以及root用户下的bash(pid = 2090)和black用户下的bash(pid = 2107)。

    上面的ps ajf命令中,参数a表示将所有与终端相关联的进程都显示出来(如果再配合x参数的话,还可以将所有不与终端相关联的进程也显示出来)。参数j表示按照job control(作业控制)的格式来进行显示,也就是除了会显示PID(进程的ID)外,还会将每个进程的PPID(父进程的PID),PGID(进程组ID),SID(会话ID)也显示出来,此外,TPGID表示进程所在的会话中,与终端进行交互的前台进程组的进程组ID。参数f表示COMMAND列部分,以类似缩进的层次感,来显示进程之间的父子关系。

    有了这些输出信息,我们就可以画出下面这一副job control(作业控制)图:


图7

    可以看到,会话(SID=2090)中包含了4个进程组,bash(pid=2090)是该会话中的第一个进程,因此,该进程就是会话的leader。前三个进程组中都只包含了一个进程,最后一个进程组(PGID=2114)中包含了两个进程,这两个进程是父子关系。此外,由于父进程(2114)是进程组中的第一个进程,因此父进程(2114)就是该进程组的leader。

    2114进程组是前台进程组,负责与pts/0的伪终端进行交互。因此,前面ps ajf命令的输出结果中,TPGID列的值就是2114。

    当在pts/0中按下ctrl+c组合键时,2114前台进程组里的进程就都会被kill掉。当Xfce Terminal将pts/0关闭掉时,与pts/0相关的会话里的所有进程都会被kill掉。这就是job control中进程组与会话的一个重要意义,可以实现对多个相关的进程进行统一的控制。

    我们接下来要介绍的os模块的tcgetpgrp方法就是用于获取某个终端所对应的TPGID值的,也就是获取前台进程组的进程组ID。而tcsetpgrp方法则是用于设置会话的前台进程组的。此外,还有一个ttyname方法可以根据文件描述符来获取终端的路径信息。这三个方法的语法格式如下(都只存在于Unix系统中):

os.tcgetpgrp(fd) -> pgid
os.tcsetpgrp(fd, pgid)
os.ttyname(fd) -> string

    上面的fd参数都表示与终端相关的文件描述符,pgid表示进程组ID。对于tcgetpgrp,会返回前台进程组的PGID。对于tcsetpgrp则会将pgid参数所对应的进程组设置为当前会话的前台进程组,当然要设置的进程组必须位于当前会话中。ttyname会以string字符串的形式,返回终端的路径信息。

    下面是一个简单的测试脚本(用于测试tcgetpgrp与ttyname方法):

# tc.py

import os,sys

tty_fd = os.open('/dev/tty', os.O_RDONLY)

tty_name = os.ttyname(sys.stdin.fileno())
print 'current tty:', tty_name
pid = os.fork()
if(pid):
    print 'parent process:', os.getpid()
    print 'parent in process group:', os.getpgid(0)
    os.waitpid(pid,0)
else:
    print 'child process:', os.getpid()
    print 'child in process group:', os.getpgid(0)
    print 'tty foreground process group:', os.tcgetpgrp(tty_fd)
    sys.exit(0)


    上面脚本在打开/dev/tty文件时,系统会自动将当前进程所关联的终端的文件描述符返回。这段代码的执行结果如下:

black@slack:~/test/tty$ python tc.py
current tty: /dev/pts/0
parent process: 2389
parent in process group: 2389
child process: 2390
child in process group: 2389
tty foreground process group: 2389
black@slack:~/test/tty$ 


    进程2389与子进程2390都处于2389的进程组中,并且该进程组就是与终端交互的前台进程组。

    下面这个脚本主要用于测试tcsetpgrp方法:

#tc2.py

import os, sys,signal, time

tty_fd = os.open('/dev/tty', os.O_RDONLY)

tty_name = os.ttyname(sys.stdin.fileno())
print 'current tty:', tty_name

pid = os.fork()
if(pid):
    print 'parent process:', os.getpid()
    print 'parent in process group:', os.getpgid(0) 
    os.waitpid(pid,0)
    print ("hello, I'm back!")
else:
    print 'child process:', os.getpid()
    print 'child in process group:', os.getpgid(0)
    print 'tty foreground process group:', os.tcgetpgrp(tty_fd)
    print 'detach child process group and then set tty to child process group'
    os.setpgid(0, 0)
    signal.signal(signal.SIGTTIN, signal.SIG_IGN)
    signal.signal(signal.SIGTTOU, signal.SIG_IGN)
    os.tcsetpgrp(tty_fd, os.getpgrp())
    print 'child in process group:', os.getpgid(0)
    print 'tty foreground process group:', os.tcgetpgrp(tty_fd)
    print "I'm child, I have controlled the tty!"
    print "parent will go to background"
    sys.exit(0)


    代码中会将子进程分离出去,让子进程处于自己新建的进程组中(通过os.setpgid(0, 0)来实现),然后,通过tcsetpgrp方法将当前会话的前台进程组设置为子进程所在的进程组,那么父进程所在的进程组就会成为后台进程组。当父进程试图向终端写入数据时,就会收到SIGTTOU信号(前提是终端使用了stty tostop命令),在该信号的作用下,父进程就会被挂起,可以通过fg命令让父进程的进程组重新成为前台进程组,并继续执行。脚本的执行结果如下:

black@slack:~/test/tty$ stty tostop
black@slack:~/test/tty$ stty
speed 38400 baud; line = 0;
eol = M-^?; eol2 = M-^?; swtch = M-^?;
ixany iutf8
tostop
black@slack:~/test/tty$ python tc2.py
current tty: /dev/pts/0
parent process: 2410
parent in process group: 2410
child process: 2411
child in process group: 2410
tty foreground process group: 2410
detach child process group and then set tty to child process group
child in process group: 2411
tty foreground process group: 2411
I'm child, I have controlled the tty!
parent will go to background

[1]+  Stopped                 python tc2.py
black@slack:~/test/tty$ bg  
[1]+ python tc2.py &
black@slack:~/test/tty$ fg
python tc2.py
hello, I'm back!
black@slack:~/test/tty$ stty -tostop
black@slack:~/test/tty$ stty
speed 38400 baud; line = 0;
eol = M-^?; eol2 = M-^?; swtch = M-^?;
ixany iutf8
black@slack:~/test/tty$ 


    通过stty -tostop可以取消tostop,取消后,后台进程就可以向终端写入数据而不被挂起了。

os模块的tempnam方法:

    tempnam方法可以为临时文件生成一个有效的文件名,然后,你就可以使用该文件名来创建临时文件(此方法并不会为你自动创建文件)。tempnam方法的语法格式如下:

os.tempnam([dir[, prefix]]) -> string

    有两个可选参数,其中dir用于指定自定义的临时目录。prefix用于指定生成的文件名前缀。如果你不需要某个参数,可以省略掉该参数,也可以将该参数设置为None来表示不需要此参数。此方法在Python 3.x的版本中已经被移除了,Python 3.x中需要使用tempfile模块来实现类似的功能。

    由于在Linux系统中,此方法最终会通过tempnam的C标准库函数去执行具体的操作,因此可以通过man tempnam来查看其详情:

black@slack:~/test/tmpnam$ man tempnam
TEMPNAM(3)                       Linux Programmer's Manual                      TEMPNAM(3)

NAME
       tempnam - create a name for a temporary file

SYNOPSIS
       #include <stdio.h>

       char *tempnam(const char *dir, const char *pfx);

       .............................................

DESCRIPTION
       The  tempnam() function returns a pointer to a string that is a valid filename, and
       such that a file with this name did not exist when tempnam() checked.  The filename
       suffix  of  the  pathname  generated  will start with pfx in case pfx is a non-NULL
       string of at most five bytes.  The directory prefix part of the pathname  generated
       is required to be "appropriate" (often that at least implies writable).

       Attempts to find an appropriate directory go through the following steps:

       a) In  case  the  environment  variable  TMPDIR  exists and contains the name of an
          appropriate directory, that is used.

       b) Otherwise, if the dir argument is non-NULL and appropriate, it is used.

       c) Otherwise, P_tmpdir (as defined in <stdio.h>) is used when appropriate.

       d) Finally an implementation-defined directory may be used.

       .............................................
black@slack:~/test/tmpnam$ 


    可以看到,tempnam会使用以下几个步骤来找到合适的临时目录:

    a) 如果存在TMPDIR环境变量的话,就会使用该环境变量的路径信息作为临时目录。

    b) 如果没有TMPDIR环境变量,就会使用tempnam的参数dir作为临时目录。

    c) 如果前两步都没有找到合适的目录的话,就会使用glibc中定义的P_tmpdir宏所对应的目录。一般到这一步就结束了,因为C标准库中都会定义这个宏。

    windows系统中的查询步骤也差不多,只不过,windows中会使用TMP环境变量。此外,P_tmpdir在Linux系统中通常为/tmp,也就是Linux系统的常规的临时目录。P_tmpdir在windows中则为'\',也就是所在分区的根目录位置,如C:\,E:\,G:\之类的分区根目录位置。

    在tempnam找到合适的临时目录后,它会生成一个在该目录中唯一的带有随机字符的文件名。所谓唯一的文件名,就是指在tempnam方法执行时,这个文件名并不存在于临时目录中。接着,你就可以使用该文件名,再配合open之类的方法去创建该文件了。

    可以看到,使用tempnam来创建临时文件是有安全隐患的。假设你在/tmp这个公共目录中生成了一个唯一的文件名,但是在你准备使用该文件名去创建临时文件时,另外一个程序比你先一步创建了该文件的话。那么,在你使用open方法打开该文件时,就会把别的程序所创建的文件给打开了。如果你向这个文件中写入一些隐私数据(例如一些账户密码之类的数据)的话,那么别的程序就可以看到你写入的隐私数据了。

    所以使用该方法时,尽量在只有自己可以访问到的目录中生成临时文件,或者在使用os.open方法时,在第二个flags参数中加入os.O_EXCL标志,有了该标志后,当文件存在时(其他程序比你先一步创建了该文件)就会抛出异常。

    当你向tempnam提供prefix参数时,在Linux系统中最多只会使用该参数的前5个字符作为文件名的前缀。windows中则会使用完整的参数作为前缀。

    下面是一个测试脚本:

# tempnam.py
import os,sys

if os.name == 'posix':
    if('TMPDIR' in os.environ):
        print 'environment TMPDIR:', os.environ['TMPDIR']
    else:
        print 'environment have no TMPDIR'
elif os.name == 'nt':
    if('TMP' in os.environ):
        print 'environment TMP:', os.environ['TMP']
    else:
        print 'environment have no TMP'

if len(sys.argv) == 1:
    usr_dir = ''
    usr_pfx = ''
    tmp_pathname = os.tempnam()
elif len(sys.argv) == 2:
    usr_dir = sys.argv[1]
    usr_pfx = ''
    tmp_pathname = os.tempnam(sys.argv[1])
elif len(sys.argv) >= 3:
    usr_dir = sys.argv[1]
    usr_pfx = sys.argv[2]
    tmp_pathname = os.tempnam(sys.argv[1], sys.argv[2])

print 'usr dir:', usr_dir
print 'usr pfx:', usr_pfx
print 'tempnam return:', tmp_pathname


    这个脚本在Linux中的测试结果如下:

black@slack:~/test/tmpnam$ python tempnam.py 
environment have no TMPDIR
tempnam.py:18: RuntimeWarning: tempnam is a potential security risk to your program
  tmp_pathname = os.tempnam()
usr dir: 
usr pfx: 
tempnam return: /tmp/file9YywQf
black@slack:~/test/tmpnam$ python -W ignore tempnam.py
environment have no TMPDIR
usr dir: 
usr pfx: 
tempnam return: /tmp/fileLDfIwb
black@slack:~/test/tmpnam$ python -W ignore tempnam.py mytmpdir
environment have no TMPDIR
usr dir: mytmpdir
usr pfx: 
tempnam return: mytmpdir/fileT1KtaO
black@slack:~/test/tmpnam$ export TMPDIR=/home/black/test/tmpdir
black@slack:~/test/tmpnam$ python -W ignore tempnam.py mytmpdir myprefix
environment TMPDIR: /home/black/test/tmpdir
usr dir: mytmpdir
usr pfx: myprefix
tempnam return: /home/black/test/tmpdir/mypre1NmKqf
black@slack:~/test/tmpnam$ 


    由于tempnam在执行时,python默认会产生一个提示该方法存在安全隐患的警告信息。因此,上面就用到了-W ignore参数来忽略掉该警告信息。可以看到,在既没有TMPDIR环境变量,又没有提供dir参数时,就会使用/tmp目录。当提供了dir参数时,在没有TMPDIR环境变量的情况下,就会使用dir参数(如上面的mytmpdir)。在提供了TMPDIR环境变量时,就只会使用该环境变量的路径作为临时目录。

    当然,你提供的目录必须是存在的,并且是当前进程有权限访问到的目录。如果提供的目录不存在的话,就会忽略掉该目录,如果目录无权限访问的话,则会抛出MemoryError的错误。例如,假设你的tmpnam目录中没有mytmpdir目录的话,那么即使你将mytmpdir作为tempnam的dir参数,tempnam也会跳过它(因为mytmpdir并不存在),因此,你在测试tempnam.py脚本时,一定要先创建好临时目录,再去设置dir参数或者设置TMPDIR环境变量。

    还可以看到,虽然提供了myprefix作为文件名的前缀,但是,Linux系统中最多只会使用前5个字符即mypre作为文件名的前缀。

    windows系统中的执行情况如下:

G:\Python27\mytest\tmpnam>..\..\python.exe -W ignore tempnam.py
environment TMP: C:\Users\ADMINI~1.WIN\AppData\Local\Temp
usr dir:
usr pfx:
tempnam return: C:\Users\ADMINI~1.WIN\AppData\Local\Temp\2

G:\Python27\mytest\tmpnam>set TMP=

G:\Python27\mytest\tmpnam>..\..\python.exe -W ignore tempnam.py tmpdir
environment have no TMP
usr dir: tmpdir
usr pfx:
tempnam return: tmpdir\4

G:\Python27\mytest\tmpnam>..\..\python.exe -W ignore tempnam.py tmpdir prexxxi
environment have no TMP
usr dir: tmpdir
usr pfx: prexxxi
tempnam return: tmpdir\prexxxi2

G:\Python27\mytest\tmpnam>..\..\python.exe -W ignore tempnam.py
environment have no TMP
usr dir:
usr pfx:
tempnam return: \2

G:\Python27\mytest\tmpnam>


    windows中,TMP环境变量默认就被设置到了当前用户的AppData\Local\Temp目录。当环境变量被移除后,才会使用tempnam方法中提供的dir参数。如果dir参数也没提供的话,就会使用'\'即磁盘分区的根目录。windows会使用完整的prefix参数作为文件名的前缀,如上面的prexxxi 。

os模块的tmpnam方法:

    tmpnam类似于tempnam,也可以为临时文件生成一个有效的文件名,它也不会为你自动创建文件。你需要根据它返回的文件名,通过open之类的方法去手动创建临时文件,其语法格式如下:

os.tmpnam() -> string

    可以看到,它并没有任何输入参数。因此,它并不能像tempnam那样自定义临时目录的位置。在Linux系统中,它只会生成/tmp目录中的临时文件名。在windows系统中,则只会生成'\'即分区根目录的临时文件名。此方法在Python 3.x的版本中已经被移除了,Python 3.x中需要使用tempfile模块来实现类似的功能。该方法与tempnam方法一样存在安全风险(具体的风险因素请参考上面的tempnam方法)。

    下面是一个简单的例子:

#tmpnam.py
import os

print 'tmpnam:', os.tmpnam()


    这个脚本在Linux中的执行情况如下:

black@slack:~/test/tmpnam$ python -W ignore tmpnam.py
tmpnam: /tmp/filekYP7GF
black@slack:~/test/tmpnam$ 


    在windows中的执行情况如下:

G:\Python27\mytest\tmpnam>..\..\python.exe -W ignore tmpnam.py
tmpnam: \s6cg.

G:\Python27\mytest\tmpnam>


os模块的tmpfile方法:

    tmpfile方法会自动为你创建一个临时文件,并且该临时文件,会在文件关闭后自动被系统删除掉。tmpfile的语法格式如下:

os.tmpfile() -> file object

    它会将临时文件所对应的文件对象作为结果返回。此方法在Python 3.x的版本中也被移除了,Python 3.x中需要使用tempfile模块来实现类似的功能。

    下面是一个简单的测试脚本:

# tmpfile.py
import os,stat,sys

def mytmpfile():
    name = os.tmpnam()
    fd = os.open(name, os.O_CREAT | os.O_EXCL | os.O_RDWR, \
            stat.S_IRUSR | stat.S_IWUSR)
    os.unlink(name)
    fo = os.fdopen(fd, "w+b")
    return fo

print 'process pid:', os.getpid()
tmp_fo = os.tmpfile()
tmp_fo.write('hello world! this is a tmp file, this file is mark as deleted!')
tmp_fo.seek(0)
print 'tmp content:', tmp_fo.read()

mytmp_fo = mytmpfile()
mytmp_fo.write('this is my tmp file generate by mytmpfile function!')
mytmp_fo.seek(0)
print 'mytmp content:', mytmp_fo.read()

raw_input('before exit:')


    脚本中定义的mytmpfile函数,是我自定义的一个用于模拟os.tmpfile方法的函数。该函数创建的临时文件也会在程序结束时被自动删除掉。原理就是os.unlink在执行删除文件的操作时,如果该文件正在被某个进程使用的话,那么这个文件就不会被马上删除,程序中还可以继续使用此文件,直到文件关闭了,或者进程结束了,系统才会彻底的删除掉文件。这样就实现了进程结束后,自动删除临时文件的功能。当然这个mytmpfile函数只能在Linux系统中这么使用,如果放在windows系统中,那么当需要删除的文件正在被某个进程使用时,会抛出异常。

    为了能够查看os.tmpfile方法到底创建了哪个临时文件,我们在脚本结束之前,用raw_input('before exit:')语句将脚本暂停。接着就可以在进程的/proc/<pid>/fd目录中查看到它打开的临时文件了:

    要测试脚本的话,需要开两个终端,第一个终端运行tmpfile.py脚本,在出现'before exit:'的提示后,在另一个终端内查看进程打开的文件描述符:

black@slack:~/test/tmpnam$ python -W ignore tmpfile.py
process pid: 2786
tmp content: hello world! this is a tmp file, this file is mark as deleted!
mytmp content: this is my tmp file generate by mytmpfile function!
before exit:


black@slack:~/test/tmpnam$ ls -l /proc/2786/fd 
total 0
lrwx------ 1 black users 64 Dec 26 18:33 0 -> /dev/pts/0
lrwx------ 1 black users 64 Dec 26 18:33 1 -> /dev/pts/0
lrwx------ 1 black users 64 Dec 26 18:33 2 -> /dev/pts/0
lrwx------ 1 black users 64 Dec 26 18:33 3 -> /tmp/tmpfuP8mTB (deleted)
lrwx------ 1 black users 64 Dec 26 18:33 4 -> /tmp/filekrL1D3 (deleted)
black@slack:~/test/tmpnam$ ls /tmp
SBo			     gtksourceview-2.10.5-i486-1_SBo.tgz  ssh-fSmJFjzN1901
blueman-applet-0	     libburn-1.3.0-i486-1_SBo.tgz	  ssh-fTURzUWB1903
bochs-2.6-i486-1_SBo.tgz     libisoburn-1.2.0-i486-1_SBo.tgz	  ssh-ibkgrSFP1914
cc5eJJOX.c		     libisofs-1.3.0-i486-1_SBo.tgz	  ssh-nRwzwmsp1820
ccETkXJm.le		     orbit-root				  ssh-oKCyaakR1910
ccdE2g6y.ld		     scim-helper-manager-socket-root	  ssh-xcXUOaPk1905
ccmLaxsL.o		     scim-panel-socket:0-root		  test_socket
gedit-2.30.4-i486-1_SBo.tgz  scim-socket-frontend-root		  tmp.XXXXSUfQTT
gedit.root.2434710519	     ssh-EnXDtIBG1881			  tmp.XXXXqjA3ST
grub2-1.99-i486-1_SBo.tgz    ssh-GCJBlAkI1897			  vboxguest-Module.symvers
black@slack:~/test/tmpnam$ 


    在2786进程所创建的临时文件中,/tmp/tmpfuP8mTB是os.tmpfile方法所生成的(该方法所生成的临时文件名的前缀是tmpf......),/tmp/filekrL1D3则是我自定义的mytmpfile函数创建的。可以看到,这些临时文件都被标记为了(deleted),它们在目录入口中的文件名都被删除了,只有文件的存储空间还处于占用状态。因此,我们无法在文件系统中查看到这些临时文件,上面的ls /tmp命令输出的结果中也就找不到这两个文件了。当2786进程结束后,这两个临时文件的存储空间就会被彻底释放掉。

    windows中使用os.tmpfile方法所创建的临时文件,也会在进程结束时,被自动删除掉,例如下面这个脚本:

#tmpfile.py
import os

tmp_fo = os.tmpfile()
tmp_fo.write('this is a tmp file, this file can be automatically deleted!')
tmp_fo.seek(0)
print 'tmp content:', tmp_fo.read()

raw_input('before exit:')


    windows中的执行情况如下:

G:\Python27\mytest\tmpnam>..\..\python.exe tmpfile.py
tmp content: this is a tmp file, this file can be automatically deleted!
before exit:


G:\>dir
....................................................
2015/12/26  19:40                 0 t3ak
....................................................

G:\>


    可以看到,它会在G盘的根目录中创建一个t3ak的临时文件,当tmpfile.py所在的进程结束后,该临时文件也会被系统自动删除掉。windows中使用os.tmpfile方法所创建的临时文件,我们还可以在文件系统中找到它。不像在Linux中那样,os.tmpfile所创建的临时文件无法通过文件系统访问到(当然,这样也增加了临时文件的安全性)。

os模块的unlink方法:

    在前面的tmpfile.py脚本的mytmpfile函数中,我们就用到过os.unlink方法,该方法可以将指定的文件给删除掉。其语法格式如下:

os.unlink(path)

    path用于指定需要删除的文件的路径。path不可以是目录,如果是目录的话,unlink在执行时就会抛出异常。os.unlink与之前文章中介绍过的os.remove方法,在本质上是同一个方法,因为它们都是通过底层的posix_unlink这个C函数去执行删除操作的(该C函数也定义在Python源代码的Modules/posixmodule.c文件中):

PyDoc_STRVAR(posix_unlink__doc__,
"unlink(path)\n\n\
Remove a file (same as remove(path)).");

PyDoc_STRVAR(posix_remove__doc__,
"remove(path)\n\n\
Remove a file (same as unlink(path)).");

static PyObject *
posix_unlink(PyObject *self, PyObject *args)
{
#ifdef MS_WINDOWS
    return win32_1str(args, "remove", "s:remove", DeleteFileA, "U:remove", DeleteFileW);
#else
    return posix_1str(args, "et:remove", unlink);
#endif
}


    在Unix系统中,它会使用unlink系统调用去执行具体的删除操作。在windows系统中,则会通过DeleteFileA或者DeleteFileW的API接口函数去执行删除操作。

    对于unix系统,unlink系统调用在删除文件时,如果该文件正在被某个进程使用的话,那么它只会将文件在目录入口中的文件名删除掉,但是文件的存储空间还处于占用状态,此时的文件还可以被进程继续使用,直到与该文件相关的文件描述符都被关闭了,才会彻底的删除掉该文件,并释放掉文件的存储空间,这在之前介绍tmpfile方法的例子时,我们已经看到了这种情况。

    而对于windows系统,DeleteFile接口函数在删除文件时,如果文件正在被使用的话,则会产生错误,因此,windows中的os.unlink方法不能像Linux中那样实现延迟删除的操作。

    在Unix系统中,如果path参数对应的是一个符号链接的话,则只会将符号链接删除掉,不会对符号链接所指向的目标文件产生影响。

    下面是一个简单的测试脚本:

# unlink.py
import os

if os.path.exists('test.txt'):
    os.unlink('test.txt')
    print 'unlink test.txt'
if os.path.exists(os.getcwd() + '/test2.txt'):
    os.remove(os.getcwd() + '/test2.txt')
    print 'remove ' + os.getcwd() + '/test2.txt'


    unlink与remove方法的path参数可以是相对路径,也可以是绝对路径。这个脚本的执行结果如下:

black@slack:~/test/unlink$ touch test.txt
black@slack:~/test/unlink$ touch test2.txt
black@slack:~/test/unlink$ python unlink.py
unlink test.txt
remove /home/black/test/unlink/test2.txt
black@slack:~/test/unlink$ ls -l
total 4
-rw-r--r-- 1 black users 235 Dec 27 12:35 unlink.py
black@slack:~/test/unlink$ 


os模块的utime方法:

    utime方法可以设置文件的atime(访问时间)与mtime(修改时间),其语法格式如下:

os.utime(path, (atime, mtime))
os.utime(path, None)

    path用于指定文件的路径。当第二个参数是一个元组时,元组中的atime用于设置文件的访问时间,mtime则用于设置文件的修改时间。当第二个参数是None时,就会将文件的访问和修改时间设置为当前时间,其作用相当于unix系统中的touch工具。第二种None格式是在Python 2.0版本中新增的。

    path参数既可以指定文件,也可以用于指定目录。

    下面是一个简单的测试脚本:

# utime.py
import os, time

if not os.path.exists('test.txt'):
    fo = open('test.txt', 'w')
    fo.close()
tm = time.strptime('2015-11-11 13:14:15', '%Y-%m-%d %H:%M:%S')
atime_sec = time.mktime(tm)
tm = time.strptime('2015-12-12 14:15:16', '%Y-%m-%d %H:%M:%S')
mtime_sec = time.mktime(tm)

os.utime('test.txt', (atime_sec, mtime_sec))
print 'set test.txt atime to 2015-11-11 13:14:15'
print 'set test.txt mtime to 2015-12-12 14:15:16'


    utime在设置时间时需要接受时间戳为参数,因此,上面就用到了time.mktime方法来生成时间戳。这个脚本的测试结果如下:

black@slack:~/test/utime$ touch test.txt
black@slack:~/test/utime$ python utime.py
set test.txt atime to 2015-11-11 13:14:15
set test.txt mtime to 2015-12-12 14:15:16
black@slack:~/test/utime$ stat test.txt
  File: `test.txt'
  Size: 0         	Blocks: 0          IO Block: 4096   regular empty file
Device: 803h/2051d	Inode: 419318      Links: 1
Access: (0644/-rw-r--r--)  Uid: ( 1001/   black)   Gid: (  100/   users)
Access: 2015-11-11 13:14:15.000000000 +0800
Modify: 2015-12-12 14:15:16.000000000 +0800
Change: 2015-12-27 13:06:32.681778537 +0800
 Birth: -
black@slack:~/test/utime$ 


    如果将utime的第二个参数设置为None,那么就可以将访问与修改时间设置为当前时间,例如下面这个脚本:

# utime2.py
import os

os.utime('test.txt', None)
print 'set test.txt atime and mtime to current time'


    执行结果如下:

black@slack:~/test/utime$ python utime2.py
set test.txt atime and mtime to current time
black@slack:~/test/utime$ stat test.txt
  File: `test.txt'
  Size: 0         	Blocks: 0          IO Block: 4096   regular empty file
Device: 803h/2051d	Inode: 419318      Links: 1
Access: (0644/-rw-r--r--)  Uid: ( 1001/   black)   Gid: (  100/   users)
Access: 2015-12-27 13:23:05.680778539 +0800
Modify: 2015-12-27 13:23:05.680778539 +0800
Change: 2015-12-27 13:23:05.680778539 +0800
 Birth: -
black@slack:~/test/utime$ 


    utime方法还可以设置目录的访问与修改时间,例如下面这个脚本:

# utime3.py

import os

import os, time

if not os.path.exists('testdir'):
    os.mkdir('testdir')

tm = time.strptime('2015-11-11 13:14:15', '%Y-%m-%d %H:%M:%S')
atime_sec = time.mktime(tm)
tm = time.strptime('2015-12-12 14:15:16', '%Y-%m-%d %H:%M:%S')
mtime_sec = time.mktime(tm)

os.utime('testdir', (atime_sec, mtime_sec))
print 'try to set testdir atime to 2015-11-11 13:14:15'
print 'try to set testdir mtime to 2015-12-12 14:15:16'


    这个脚本的执行结果如下:

black@slack:~/test/utime$ python utime3.py
try to set testdir atime to 2015-11-11 13:14:15
try to set testdir mtime to 2015-12-12 14:15:16
black@slack:~/test/utime$ stat testdir
  File: `testdir'
  Size: 4096      	Blocks: 8          IO Block: 4096   directory
Device: 803h/2051d	Inode: 419320      Links: 2
Access: (0755/drwxr-xr-x)  Uid: ( 1001/   black)   Gid: (  100/   users)
Access: 2015-11-11 13:14:15.000000000 +0800
Modify: 2015-12-12 14:15:16.000000000 +0800
Change: 2015-12-27 13:30:37.238778538 +0800
 Birth: -
black@slack:~/test/utime$ 


    虽然官方手册上说utime在windows中不能以目录作为path参数,但是测试结果是,在作者的win7系统中,对目录的访问与修改时间进行设置也是可以的。windows中,在设置完后,可以通过上一篇文章中介绍过的跨平台的stat程式来查看其atime与mtime的信息:

G:\Python27\mytest\utime>..\..\python.exe utime3.py
try to set testdir atime to 2015-11-11 13:14:15
try to set testdir mtime to 2015-12-12 14:15:16

G:\Python27\mytest\utime>..\..\python.exe stat.py lstat testdir
  File: testdir [directory]
  Size: 0
Device: 0x0/0d     Inode: 0     Links: 0
Access: (0777/drwxrwxrwx)   Uid: 0   Gid: 0
Access: 2015-11-11 13:14:15
Modify: 2015-12-12 14:15:16
Change: 2015-12-27 13:41:39

G:\Python27\mytest\utime>


os模块的walk方法:

    walk方法在执行时,返回的是一个generator对象,该对象只有在迭代操作时才会去执行walk里的代码,每迭代一次就会返回一层目录结构。walk除了会将最上层的目录结构迭代出来外,还会将子目录,以及子目录的子目录中的目录结构也迭代出来。在作者的系统中,walk方法的python源代码定义在/usr/local/lib/python2.7/os.py文件里:

def walk(top, topdown=True, onerror=None, followlinks=False):
    """Directory tree generator.

    For each directory in the directory tree rooted at top (including top
    itself, but excluding '.' and '..'), yields a 3-tuple

        dirpath, dirnames, filenames

    dirpath is a string, the path to the directory.  dirnames is a list of
    the names of the subdirectories in dirpath (excluding '.' and '..').
    filenames is a list of the names of the non-directory files in dirpath.
    Note that the names in the lists are just names, with no path components.
    To get a full path (which begins with top) to a file or directory in
    dirpath, do os.path.join(dirpath, name).

    If optional arg 'topdown' is true or not specified, the triple for a
    directory is generated before the triples for any of its subdirectories
    (directories are generated top down).  If topdown is false, the triple
    for a directory is generated after the triples for all of its
    subdirectories (directories are generated bottom up).

    When topdown is true, the caller can modify the dirnames list in-place
    (e.g., via del or slice assignment), and walk will only recurse into the
    subdirectories whose names remain in dirnames; this can be used to prune the
    search, or to impose a specific order of visiting.  Modifying dirnames when
    topdown is false is ineffective, since the directories in dirnames have
    already been generated by the time dirnames itself is generated. No matter
    the value of topdown, the list of subdirectories is retrieved before the
    tuples for the directory and its subdirectories are generated.

    By default errors from the os.listdir() call are ignored.  If
    optional arg 'onerror' is specified, it should be a function; it
    will be called with one argument, an os.error instance.  It can
    report the error to continue with the walk, or raise the exception
    to abort the walk.  Note that the filename is available as the
    filename attribute of the exception object.

    By default, os.walk does not follow symbolic links to subdirectories on
    systems that support them.  In order to get this functionality, set the
    optional argument 'followlinks' to true.

    Caution:  if you pass a relative pathname for top, don't change the
    current working directory between resumptions of walk.  walk never
    changes the current directory, and assumes that the client doesn't
    either.

    Example:

    import os
    from os.path import join, getsize
    for root, dirs, files in os.walk('python/Lib/email'):
        print root, "consumes",
        print sum([getsize(join(root, name)) for name in files]),
        print "bytes in", len(files), "non-directory files"
        if 'CVS' in dirs:
            dirs.remove('CVS')  # don't visit CVS directories

    """

    islink, join, isdir = path.islink, path.join, path.isdir

    # We may not have read permission for top, in which case we can't
    # get a list of the files the directory contains.  os.path.walk
    # always suppressed the exception then, rather than blow up for a
    # minor reason when (say) a thousand readable directories are still
    # left to visit.  That logic is copied here.
    try:
        # Note that listdir and error are globals in this module due
        # to earlier import-*.
        names = listdir(top)
    except error, err:
        if onerror is not None:
            onerror(err)
        return

    dirs, nondirs = [], []
    for name in names:
        if isdir(join(top, name)):
            dirs.append(name)
        else:
            nondirs.append(name)

    if topdown:
        yield top, dirs, nondirs
    for name in dirs:
        new_path = join(top, name)
        if followlinks or not islink(new_path):
            for x in walk(new_path, topdown, onerror, followlinks):
                yield x
    if not topdown:
        yield top, dirs, nondirs


    源码的注释中已经介绍的很详细了。

    上面的walk方法中包含了yield关键字,该关键字的作用类似于return,但是和return所不同的是,return在执行后,该方法就“彻底”返回了,下一次再执行walk时又要从头开始执行,而yield关键字可以将walk方法变为generator对象,generator对象每执行一次迭代操作,就会去执行一次walk方法,当walk执行到yield时,yield也会将yield后面的表达式作为结果返回,但是在返回时,generator对象还会将yield之后的下一条语句的执行位置,和当前函数的状态信息保存下来。那么下一次对generator对象执行迭代操作时,就会直接从yield之后的下一条语句开始执行(而不会像return那样又要从头开始执行)。

    这样,walk方法就不需要将所有的结果一次返回,而是可以每迭代一次,就只用yield返回一个结果(下一次迭代时,就从yield后面继续执行,直到遇到yield后又返回一个结果,再下一次执行时,再从yield后执行一段代码,再次遇到yield后,再返回一个结果,以此类推),这样可以以牺牲性能的方式,来节省一定程度上的内存上的开销(如果采用return的话,那么walk就需要返回一个包含了所有结果的很大的列表, 这会占用很多内存)。

    walk方法每迭代一次,yield返回的会是一个包含了三个成员的元组: (dirpath, dirnames, filenames),dirpath表示当前walk所处的目录的路径,dirnames表示dirpath目录中包含的子目录(子目录包含在一个列表中),filenames表示dirpath目录中包含的文件(文件也包含在一个列表中)。通过dirnames和filenames,我们就可以知道dirpath目录中包含了哪些子目录和哪些文件了。

    walk方法有四个参数:第一个top参数表示顶层的目录路径,walk会将top及top中包含的子目录,以及子目录的子目录里的目录结构等,在每次迭代的过程中依次返回。

    第二个topdown参数,表示从顶层往下返回,还是从底层往上返回,如果topdown为True(缺省值就是True),那么,会先返回顶层的目录结构,再返回子目录的目录结构,然后返回子目录的子目录的目录结构,以此类推。如果topdown为False,则会先进入子目录,先将子目录的目录结构返回,再返回上一层的目录结构。

    第三个onerror参数,用于指定一个自定义的错误处理函数,当walk在对目录结构进行“游走”时,如果遇到了错误时(比如某个目录无权限打开时),就会调用onerror对应的用户自定义函数去处理错误信息。

    第四个followlinks参数,表示是否“游走”进入符号链接所指向的目录,缺省值为False,也就是不进入符号链接。如果是True,就会进入符号链接所指向的目录,从而把这些目录中的目录结构也“游走”一遍。

    下面是一个简单的测试脚本:

# walk.py
import os

print os.walk('walk')

def error_func(err):
    print 'my error_func:',err

for i in os.walk('walk', True, onerror = error_func, followlinks = False):
    print i


    在我的walk.py脚本所在的目录内,有一个测试用的walk目录,我们就使用该脚本来对该目录内的目录结构进行显示,脚本的执行结果如下:

black@slack:~/test$ python walk.py
<generator object walk at 0xb74bbdb4>
('walk', ['mydir2', 'mydir'], ['test'])
('walk/mydir2', [], ['hello', 'world'])
('walk/mydir', ['subdir'], ['subfile'])
('walk/mydir/subdir', ['ssdir'], ['test'])
('walk/mydir/subdir/ssdir', [], ['ss_test'])
black@slack:~/test$ 


    上面是从顶层往下进行显示的,如下图所示:


图8

    可以看到,walk方法把walk目录中包含的所有的子目录和文件都“游走”了一遍。

结束语:

    与Python基本的I/O操作相关的内容,就介绍到这里,下一篇将介绍Python Exceptions(Python中的异常)。

    OK,就到这里,休息,休息一下 o(∩_∩)o~~

    更高级的哲人独处着,这并不是因为他想孤独,而是因为在他的周围找不到他的同类

——  弗里德里希·尼采
 
上下篇

下一篇: Python中的异常

上一篇: Python基本的I/O操作 (四)

相关文章

Python定义和使用模块

Python的安装与使用

Python元组类型及相关函数

Python的基本语法

Python用户自定义函数

Python词典相关的脚本函数及方法