python 标准库中包含一些 Linux 系列的专有服务 模块,fcntl 就是其中一个。本文讨论的 fcntlioctlpython 包装函数都包含在这个模块当中。

fcntl 获取操作

fcntl 的函数 C 原型如下,《Linux/Unix系统编程手册》5.2 节有介绍。fcntl 对于大多数操作来说,从返回值中就能获取结果;只有少部分需要值结果参数来完成功能。

1
int fcntl(int fd, int cmd, ... /* arg */ );

python 原型如下

1
 fcntl.fcntl(fd, cmd, arg=0)

通过 值结果参数 来获取,在参数的处理上,就会有些麻烦。

python里,传入系统 api 或者 C 库的特有数据结构指针,都是通过 struct 模块来打包成字节序列。这只解决了传入参数的问题,对于值结果参数,还有传出结果的问题。

由于 python 中的 bytes 类型是不可变类型,python 当中无法像再 C 中一样,给这些包装的 C 函数传入一个保存结果的缓存。所以 fcntl.fcntl 通过返回字节序列值解决指针传出结果的问题,这也是 python 一贯的做法,比如 read 函数返回值就是读取的内容。而在 C 当中,需要传入一个接收内容的缓存,函数返回值是实际接收的字节数。

比如 F_GETLK 获取一个文件上的锁信息操作,在 C 里这个操作需要传入一个缓存指针作为入参和出参的功能(即值结果参数)。

python 中的操作按照图中画笔标注的序号加以说明。

fcntl

  1. 获取文件 f 上的锁信息,此时 fcntl.fcntl 返回的就是一个字节序列,struct.unpack 对返回的字节序列 bytes 类型对象解包之后第一个值是 2(F_UNLCK),表示可以加锁。注:F_GETLK 准确的来说是获取是否可以对文件进行加锁。

  2. 对文件 f 进行加锁,此时 fcntl.fcntl 返回的依然是一个字节序列,是传入的参数 arg 的原样返回。此时使用 shell 命令 lslocks 可以看到 lock.txt 已经被上锁

  3. 左边另外一个 ipython 进程当中,时间表明了先后顺序。获取 f 上的文件锁信息,无论入参是 F_WRLCK 还是 F_RDLCK, 返回的都是1(F_WRLCK), 表明当前的文件区域上有写锁,且返回值的进程 id 字段标注了对该文件上锁的进程。

  4. C 中的记录锁结构如下,该结构在 C 里的大小是 32 个字节,struct.pack 没有考虑最后一个字段的填充问题,返回的是 28.

          struct flock {
              ...
              short l_type;    /* Type of lock: F_RDLCK,
                                  F_WRLCK, F_UNLCK */
              short l_whence;  /* How to interpret l_start:
                                  SEEK_SET, SEEK_CUR, SEEK_END */
              off_t l_start;   /* Starting offset for lock */
              off_t l_len;     /* Number of bytes to lock */
              pid_t l_pid;     /* PID of process blocking our lock
                                  (set by F_GETLK and F_OFD_GETLK) */
              ...
          };
    

ioctl 获取操作

ioctl 的函数 C 原型

1
int ioctl(int fd, unsigned long request, ...);

python 原型

1
fcntl.ioctl(fd, request, arg=0, mutate_flag=True)

对于大部分操作,ioctl 都是采用值结果参数 的形式来使用,python 文档如下

This function is identical to the fcntl() function, except that the argument handling is even more complicated.

The request parameter is limited to values that can fit in 32-bits. Additional constants of interest for use as the request argument can be found in the termios module, under the same names as used in the relevant C header files.

此处提到相关的操作的枚举常量都定义在 temios 模块当中,名字和 C 中的一致。

The parameter arg can be one of an integer, an object supporting the read-only buffer interface (like bytes) or an object supporting the read-write buffer interface (like bytearray).

这里提到 arg 可以是一个不可变对象,比如 bytes ;也可以是一个可变对象,比如 bytearray.

In all but the last case, behaviour is as for the fcntl() function.

arg 是一个可变对象时,此时的行为与 fcntl.fcntl 才有区别。

If a mutable buffer is passed, then the behaviour is determined by the value of the mutate_flag parameter.

If it is false, the buffer’s mutability is ignored and behaviour is as for a read-only buffer, except that the 1024 byte limit mentioned above is avoided – so long as the buffer you pass is at least as long as what the operating system wants to put there, things should work.

If mutate_flag is true (the default), then the buffer is (in effect) passed to the underlying ioctl() system call, the latter’s return code is passed back to the calling Python, and the buffer’s new contents reflect the action of the ioctl(). This is a slight simplification, because if the supplied buffer is less than 1024 bytes long it is first copied into a static buffer 1024 bytes long which is then passed to ioctl() and copied back into the supplied buffer.

即使 arg 传入的是一个可变对象,还需要依据 mutate_flag 是否为真。若为真,结果直接写入 arg 当中,此时的行为同原始的 C 接口中的值结果参数 一致;若为假,将忽略 arg 的可变性,将 arg 当做不可变对象,此时的结果同 fcntl.fcntl 一样,通过函数的返回值返回。

下边用获取 tcp 内核缓冲区中的未读取字节数为例来说明。

ioctl

首先看右边的 ipython 创建侦听 socket, 连接建立之后写入 0x1234 个字节。

回到左端,在 C 中,FIONREAD 操作用来获取内核缓冲区的字节长度,值结果参数 需要传入的是一个 int 指针类型,相当于一块 4 个字节长度缓存用来接收结果。

  1. 这里使用fcntl.ioctl 文档中给出的示例,使用一个 array.array 类型的可变对象作为值结果参数,这里故意让传入的 array.array 对象有 2 个元素,相当于传入的接收结果的缓冲区有 8 个字节可用,最终返回值只使用了前 4 个字节 – 只有第一个元素被修改,从 255 变为 4660 表明内核缓冲区中还有 4660 个字节的内容。

  2. 使用 4 个字节的 bytearray 对象作为接收结果的缓冲区,调用结束,bytearray 对象 byte_buf 的内容 b'4\x12\x00\x00' 就是小端模式的 0x1234(字符4 的 ASCII 码就是 0x34). 当使用可变对象时,fcntl.ioctl 正常返回值都是 0, 语义上也和 C 的一致。

    1
    
    struct.unpack("i", b'4\x12\x00\x00') #(4660,)
    
  3. 当传入的是可变对象,但是 mutate_flag 参数是 False 时,此时可变对象被当作不可变对象使用,结果通过 fcntl.ioctl 函数的返回值来表达。

  4. 传入不可变对象,无论是 str 还是 byte, 都是通过返回值来表达结果。当然不可变对象的长度必须契合 C 结构缓冲区的长度,因为返回值的长度和传入的不可变对象长度一致,过短将会发生截断。

    缓冲区过短

总结

  • 使用 array.array 的好处显而易见,元素带有解释的类型;不用像 bytearray 那样需要再转换成整数。

  • 无论使用可变对象还是不可变对象,都需要和 C 结构中的值结果参数所代表的缓冲区的长度一致,否则会发生截断。