为什么 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
我目前正在开发 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