为什么 Python 在打开字符设备文件时执行 `TIOCGWINSZ` ioctl 调用?

Why does Python perform an `TIOCGWINSZ` ioctl call when opening a character device file?

我目前正在开发 Linux 设备驱动程序,并且正在将整个字符设备基础设施业务部署到位;主要是无聊的东西,用处理函数填充 file_operations 结构,同时我正在 Python.

中编写一个小测试套件

内核端相关代码(这里真的看的不多)

#if LINUX_VERSION_CODE < KERNEL_VERSION(2, 6, 32)
/* We try to keep the preprocessor #if/#endif mayhem to a minimum, but
 * this is one of the few places where there's no way around it, other
 * than obfuscating the function definitions behind proprocessor mayhem
 * at a different place.
 *
 * The following two helper macros abstract away the kernel version specific
 * ioctl function prototypes and access to the file pointer inode. */

#define IOCTL_FUNC(name, _inode, _filp, _cmd, _args) \
    int name(struct inode *_inode, struct file  *_filp, unsigned int _cmd, unsigned long _args)
#define IOCTL_INODE(_filp, _inode) \
    (void)_inode;
#else
#define IOCTL_FUNC(name, _inode, _filp, _cmd, _args) \
    long name(struct file  *_filp, unsigned int _cmd, unsigned long _args)
#  define IOCTL_INODE(_filp, _inode) \
    struct inode *_inode = file_inode(_filp); \
    (void)_inode;
#endif

static
int dwdsys_dev_from_inode_or_file(
    struct inode *inode,
    struct file *filp,
    struct dwddev **out_dwd )
{
    int rc = -ENODEV;
    struct dwddev *dwd = NULL;
    struct dwdsys_linux *dsl;
    list_for_each_entry( dsl, &dwdsys_list, entry ){
        if( MAJOR(asl->devno_base) == MAJOR(inode->i_rdev) ){
            unsigned const i_board = MINOR(inode->i_rdev);
            if( i_board < dsl->ds.n_boards ){
                dwd = dsl->ds.board[i_board];
                rc= 0;
                break;
            }
        }
    }

    /* XXX: cache dwd in either filp->private_data or inode->i_private */
    if( !rc ){
        if( out_dwd ){ *out_dwd = dwd; }
    }
    return rc;
}

static IOCTL_FUNC(dwdsys_chrdev_ioctl, inode, filp, cmd, args)
{
    int rc;
    struct dwddev *dwd= NULL;
    IOCTL_INODE(filp, inode);

    rc= dwdsys_dev_from_inode_or_file(inode, filp, &dwd);
    if( rc ){ goto fail; }
    switch( cmd ){
    default:
        rc= -EINVAL;
        break;

    case ...:
        /* ... */
    }

fail:
    DWD__TRACE_FUNCTION(dwd, rc, "cmd=0x%08x, args=%p", cmd, (void*)args); 
    return rc;
}

static struct file_operations dwdsys_chrdev_fops = {
    .owner      = THIS_MODULE,
#if LINUX_VERSION_CODE < KERNEL_VERSION(2, 6, 30)
    .ioctl          = dwdsys_chrdev_ioctl,
#else
    .unlocked_ioctl = dwdsys_chrdev_ioctl,
#endif
};

我刚刚注意到,当我在 Python 交互式 REPL 中打开驱动程序的设备节点时,驱动程序将报告一个不受支持的 ioctl 调用,命令代码 0x5413 转换为 TIOCSWINSZ。那将是在 VTs/PTYs 上用于设置 window 大小的 ioctl。我明白为什么 Python REPL 会在 stdio 上执行该 ioctl。但是无条件地去做这件事似乎很奇怪。

这是我在 Python REPL

中所做的
>>> dwd = open("/dev/dwd0a", "r")

就是这样。这将使我的驱动程序向内核日志发出警告,即调用了不受支持的 ioctl。

所以问题是:这是有意的、指定的行为吗?还是这是无意的,也许应该将其报告为错误?

根据 strace-ed 会话的注释/控制台输出中的请求更新

dw@void: ~/dwd/src/linux master ⚡
$ python
Python 3.5.2 (default, Oct 19 2016, 17:19:49) 
[GCC 4.9.4] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> dwd = open("/dev/dwd0a", "r")
[1]  + 1906 suspended  python

此时,在回车之前我暂停了Python REPL进程,并在其上附加了strace,然后再次将其置于前台...

dw@void: ~/dwd/src/linux master ⚡
$ sudo strace -p 1906 &
[2] 1922
strace: Process 1906 attached                                                                                      
--- stopped by SIGTSTP ---
 
dw@void: ~/dwd/src/linux master ⚡
$ fg
[1]  - 1906 continued  python
--- SIGCONT {si_signo=SIGCONT, si_code=SI_USER, si_pid=738, si_uid=1000} ---
select(1, [0], NULL, NULL, NULL)        = 1 (in [0])
rt_sigaction(SIGWINCH, {0x7fd1524463e0, [], SA_RESTORER|SA_RESTART, 0x7fd152fbcbef}, {0x7fd152668980, [], SA_RESTORER, 0x7fd152fbcbef}, 8) = 0
read(0, "\n", 1)                        = 1
writev(1, [{iov_base="", iov_len=0}, {iov_base="\n", iov_len=1}], 2) = 1
ioctl(0, SNDCTL_TMR_STOP or TCSETSW, {B38400 opost isig icanon echo ...}) = 0
rt_sigaction(SIGWINCH, {0x7fd152668980, [], SA_RESTORER, 0x7fd152fbcbef}, {0x7fd1524463e0, [], SA_RESTORER|SA_RESTART, 0x7fd152fbcbef}, 8) = 0
open("/dev/dwd0a", O_RDONLY|O_CLOEXEC)  = 3
fcntl(3, F_SETFD, FD_CLOEXEC)           = 0
fstat(3, {st_mode=S_IFCHR|0666, st_rdev=makedev(240, 0), ...}) = 0
ioctl(3, TIOCGWINSZ, 0x7fffb1291f00)    = -1 EINVAL (Invalid argument)
lseek(3, 0, SEEK_CUR)                   = -1 ESPIPE (Invalid seek)
ioctl(3, TIOCGWINSZ, 0x7fffb1291eb0)    = -1 EINVAL (Invalid argument)
getcwd("/home/dw/dwd/src/linux", 1024) = 31
stat("/home/dw/dwd/src/linux", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0
stat("/usr/lib/python3.5", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0
stat("/usr/lib/python3.5/_bootlocale.py", {st_mode=S_IFREG|0644, st_size=1301, ...}) = 0
stat("/usr/lib/python3.5/_bootlocale.py", {st_mode=S_IFREG|0644, st_size=1301, ...}) = 0
open("/usr/lib/python3.5/__pycache__/_bootlocale.cpython-35.pyc", O_RDONLY|O_CLOEXEC) = 4
fcntl(4, F_SETFD, FD_CLOEXEC)           = 0
fstat(4, {st_mode=S_IFREG|0644, st_size=1028, ...}) = 0
lseek(4, 0, SEEK_CUR)                   = 0
fstat(4, {st_mode=S_IFREG|0644, st_size=1028, ...}) = 0
read(4, "\r\r\n53X[=14=][=14=]3[=14=][=14=][=14=][=14=][=14=][=14=][=14=][=14=][=14=][=14=][=14=][=14=]\v[=14=][=14=][=14=]@[=14=][=14=]"..., 1029) = 1028
read(4, "", 1)                          = 0
close(4)                                = 0
lseek(3, 0, SEEK_CUR)                   = -1 ESPIPE (Invalid seek)
brk(0x562c9d5c0000)                     = 0x562c9d5c0000
ioctl(0, TIOCGWINSZ, {ws_row=45, ws_col=115, ws_xpixel=809, ws_ypixel=589}) = 0
ioctl(1, TIOCGWINSZ, {ws_row=45, ws_col=115, ws_xpixel=809, ws_ypixel=589}) = 0
ioctl(0, TIOCGWINSZ, {ws_row=45, ws_col=115, ws_xpixel=809, ws_ypixel=589}) = 0
ioctl(0, TIOCSWINSZ, {ws_row=45, ws_col=115, ws_xpixel=809, ws_ypixel=589}) = 0
ioctl(0, TCGETS, {B38400 opost isig icanon echo ...}) = 0
ioctl(0, SNDCTL_TMR_STOP or TCSETSW, {B38400 opost isig -icanon -echo ...}) = 0
writev(1, [{iov_base=">>> ", iov_len=4}, {iov_base=NULL, iov_len=0}], 2>>> ) = 4

我在调用(自定义)linux 驱动程序的 ioctl 时遇到了一些类似的问题。我发现使用 os.open 可以解决我的问题。在 os.open 的描述中,他们说它是为低级 I/O 而设计的。所以也许你不应该用内置的 open() 打开设备节点,即使每个人都这样做?如果它不是带有关于未知 ioctl 的错误消息的自定义驱动程序,我永远不会意识到还有其他一些 ioctl。

使用内置的 open():

with open('/dev/hdmi_0_0_0', 'r') as fd:
    fcntl.ioctl(fd, 0x40084814, reg_acc)

=>

openat(AT_FDCWD, "/dev/hdmi_0_0_0", O_RDONLY|O_LARGEFILE|O_CLOEXEC) = 3
fstat64(3, {st_mode=S_IFCHR|0600, st_rdev=makedev(239, 0), ...}) = 0
ioctl(3, TCGETS, 0xffe55e68)            = -1 ENOSYS (Function not implemented)
_llseek(3, 0, 0xffe55d58, SEEK_CUR)     = -1 ESPIPE (Illegal seek)
ioctl(3, TCGETS, 0xffe55e08)            = -1 ENOSYS (Function not implemented)
_llseek(3, 0, 0xffe55c48, SEEK_CUR)     = -1 ESPIPE (Illegal seek)
ioctl(3, _IOC(_IOC_READ, 0x48, 0x14, 0x8), 0xffe55c38) = 0

使用os.open():

fd = os.open('/dev/hdmi_0_0_0', os.O_RDWR | os.O_SYNC)
fcntl.ioctl(fd, 0x40084814, reg_acc)
os.close(fd)

=>

openat(AT_FDCWD, "/dev/hdmi_0_0_0", O_RDWR|O_SYNC|O_LARGEFILE|O_CLOEXEC) = 3
ioctl(3, _IOC(_IOC_READ, 0x48, 0x14, 0x8), 0xffa076e8) = 0