终端是计算机系统中重要的交互工具,从最初的机械式电传打字机到现代的虚拟终端,它承载了计算机发展的许多关键技术。本文将从终端的历史谈起,介绍其发展过程中的重要阶段,接着分析终端驱动的实现原理,最后通过代码实现一个简单的终端接口,帮助读者从理论到实践更好地理解终端的工作原理。希望这篇文章能为对终端技术感兴趣的读者提供一些启发和参考。
一、终端
终端是一种输入输出设备 , 常见的终端有显示器, 键盘, 控制台等. 在老一代的机器中呢, 控制台和终端是分开的, 如下所示.
而随着时间的推移控制台已经和显示器合二为一了. 以上描述的是广义的终端设备. 而在计算机内部对于线程来说终端就是 /dev/tty* 和 /dev/consol 所描述的设备节点 .
广义上来说终端是输入输出设备, 显示器键盘
狭义上来说终端就是 dev/tty* 和 /dev/consol 所描述的设备节点
为什么要反复强调这个, 因为理解这两条概念对理解终端很重要.理解了这个我们在描述终端的时候, 就可以根据上下文来确定对应场景的终端表示的是什么.
二、终端的类型
1、虚拟终端
在计算机刚发展的时候, 计算机是非常昂贵的资源, 并不是所有人都能用得起的, 而 cpu 的性能往往是过剩的. 于是人们就想一台计算机怎么个多个人用. 于是就在一台计算机上接入了多组显示器+键盘 .
linux 为每一个用户(一组显示器+键盘)创建一个终端, 让他们能和计算机交互, 这个终端就是虚拟终端(/dev/tty1-/dev/tty63). 注意了, 每一个虚拟终端(狭义终端)都对应一组物理的外设(广义终端).
如图有三个用户, 他们分别使用了三个虚拟终端. 每个用户都可以通过 crtl + alt + fn 切换到对应的虚拟终端(/dev/ttyn) . 这个并不是一对一的关系, 也可以多个用户使用一个虚拟终端, 这取决于驱动是否支持这个功能.
2、伪终端 pty
随着计算机网络的发展,用户希望能够远程访问计算机系统. 这种需求催生了远程登录协议和工具,如 Telnet 和 rlogin,它们允许用户通过网络连接到远程计算机.为了支持这些远程访问协议,系统需要一种机制来模拟本地终端,以便远程用户能够像在本地终端一样与计算机进行交互. 于是伪终端 pty 就诞生了. 对应的设备节点 /dev/pts/*
伪终端 PTY 的机制如下所示. 伪终端分为两个部分 master 和 slave. 他们分别对接进程. 两个进程互相不知道对方的存在.他们面对的都是伪终端 pty 设备. 于是他们只需要打开设备, 读设备, 写设备, 关闭设备就行了. 这里 “进程1” 写一个数据, 数据会从主设备发送到从设备. 然后对接从设备的 “进程2” 就能读到数据. 同理也可以反过来.
终端模拟器就是利用伪终端机制实现的的应用程序 . 常见的有 xshell, mobaxterm 等等. 下图展示了终端模拟器的通讯流程.
3、串口终端
这个也是因为历史原因导致的, 在很久很久以前. 我们的终端设备显示器和键盘, 都是通过串口来传输数据的 . 因此内核的 tty_drvier 就接入了 uart_driver 用来接收串口数据. 这个东西也就一直保留到了现在. 对应的设备节点就是 /dev/ttyS*
.
4、控制终端
控制终端就是当前进程持有的终端 /dev/tty
. linux 中进程都可以和终端(狭义的终端/dev/tty*
)进行交互. 而同一时刻只能有一个进程和控制终端(/dev/tty
) 进行交互. 这个持有控制终端的进程叫做控制进程 . 可以通过 lsof /dev/tty
查看当前的控制进程.
1 2 3 lsof /dev/tty COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME zsh 1532 baron 10u CHR 5,0 0t0 9 /dev/tty
可以看出当前的控制进程为 zsh . 而 /dev/tty 关联的终端就是控制终端. 通过命令 tty
就可以知道当前被关联的终端.
当前的控制终端就是伪终端 /dev/pts/6
. 前面描述的虚拟终端(/dev/tty1
) 、伪终端(/dev/pty/*
) 、串口终端(/dev/ttyS0
) . 都能成为控制终端.谁被控制进程持有的 /dev/tty
关联, 谁就是控制终端. 下面伪代码体现了关联的过程.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 #include <stdio.h> #include <stdlib.h> #include <fcntl.h> #include <unistd.h> #include <termios.h> #include <errno.h> #include <sys/ioctl.h> int main () { int fd_ttyS0, fd_tty; fd_ttyS0 = open ("/dev/ttyS0" , O_RDWR | O_NOCTTY); if (fd_ttyS0 < 0 ) { perror ("Failed to open /dev/ttyS0" ); exit (EXIT_FAILURE); } fd_tty = open ("/dev/tty" , O_RDONLY); if (fd_tty < 0 ) { perror ("Failed to open /dev/tty" ); close (fd_ttyS0); exit (EXIT_FAILURE); } if (ioctl (fd_tty, TIOCNOTTY) < 0 ) { perror ("Failed to disconnect from current control terminal" ); close (fd_tty); close (fd_ttyS0); exit (EXIT_FAILURE); } if (ioctl (fd_ttyS0, TIOCSCTTY) < 0 ) { perror ("Failed to set /dev/ttyS0 as control terminal" ); close (fd_tty); close (fd_ttyS0); exit (EXIT_FAILURE); } close (fd_tty); close (fd_ttyS0); return 0 ; }
三、内核中的终端
虚拟终端(/dev/tty1
) 、伪终端(/dev/pty/*
) 、串口终端(/dev/ttyS0
) . 等终端在内核中都是通过 tty_driver 注册进内核的.只是对应的配置略有差别如下图所示.
终端就是通过 tty_register_driver
注册进内核的字符设备, 如下图所示为 tty driver 的链路结构. 图中蓝色的双向箭头线路, 就是数据读写的流程 . 整个过程可以分为三个部分, 字符设备(cdev) --> 线路规程(line discipline) --> tty 驱动(tty core) --> 外部. 他们在内核中的代码位置 driver/tty/*
.
终端的读写并不是直接与外部设备相连的。在 TTY 体系中,TTY driver(TTY 核心)负责与外部设备的交互,这个外部设备可能是串口设备, 也可能是虚拟终端(VT)对应的显示器.
写操作先把数据写入到线路规程, 等到合适的时机在发送出去(例如按下enter键) . 然而读操作并不是直接从外部设备读取数据 ,而是从线路规程(line discipline)的缓冲区 (n_tty_data) 中读取 。这个缓冲区的数据来源于 TTY driver >的 port->buf 缓冲区 ,它通过 flush_to_ldisc
函数将数据写入线路规程的缓冲区 n_tty_data. 而TTY driver 的 port->buf 缓冲区 又是由 VT/UART/PTY 等驱动通过 tty_insert_flip_char/tty_insert_flip_string
这样的接口主动写入的.
因此, 当用户空间调用 read 时, 实际上是检查线路规程的缓冲区 n_tty_data 中是否有数据。如果有数据, 则返回给用户;如果没有数据, 则会根据 read 的调用参数决定是否阻塞等待数据的到来.
有些串口驱动只要有数据会不断地向线路规程的缓冲区写入数据 , 这也解释了为什么在某些情况下, 打开串口设备时可能会出现一堆历史数据. 即使你没有主动读取数据, 驱动程序仍然在后台填充缓冲区, 导致一旦你打开设备并读取时, 这些积累的数据会一股脑地被读取出来.
为了避免这种情况,应用程序在打开串口后,可以使用 tcflush
命令清空线路规程的缓冲区,从而确保读取到的数据是最新的,而不是之前积累的旧数据.
1 2 3 4 5 6 7 8 9 10 11 12 13 #include <termios.h> #include <unistd.h> int fd = open ("/dev/ttyS0" , O_RDWR | O_NOCTTY);tcflush (fd, TCIFLUSH);tcflush (fd, TCOFLUSH);tcflush (fd, TCIOFLUSH);
那么为什么不直接读写要在中间加个线路规程呢, 因为终端是机器和人打交道的, 在传统的终端场景中, 人们在输入时可能会出现错误, 比如输入错字,然后希望通过退格键删除错误的输入. 或者在输入完一整行数据后按下回车键, 才将数据发送出去。线路规程就负责在用户按下回车键之前,对输入的数据进行缓冲和预处理,例如处理退格键、编辑操作、信号等 . 具体来说,线路规程会对输入进行以下处理:
字符回显 :当用户输入字符时,线路规程会将字符回显在屏幕上。
编辑操作 :支持退格键删除错误输入的字符,以及其他基本的行编辑功能。
信号处理 :处理诸如中断信号(如 Ctrl-C)等特殊输入。
行缓冲 :在用户按下回车键之前,输入的数据被缓冲起来,直到用户按下回车键,线路规程才将整行数据传递给应用程序
1、open
应用程序 open 某个 tty 设备节点, 会调用到对应的 tty_open, 主要调用流程如下.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 tty_open ()--> tty_alloc_file ()--> tty=tty_open_current_tty ()--> if (!tty){ tty = tty_driver_lookup_tty ()--> if (!tty){ retval = tty_reopen (tty); }else { tty_init_dev ()--> alloc_tty_struct ()--> tty_driver_install_tty ()--> tty->port = driver->ports[idx];--> tty->port->itty = tty; tty_ldisc_setup ()--> } } retval = tty->ops->open (tty, filp)
大体分为两种情况:
第一次打开终端会创建一个 tty_struct
设置默认的线路规程 N_tty , 增加 tty_struct
的 count
计数, 初始化 N_tty
线路规程, 最后回调 tty->ops->open(tty, filp)
第二次打开则直接获取第一次创建的 tty_struct
然后增加其引用计数. 然后回调 tty->ops->open(tty, filp)
tty_struct
是 tty_open
时创建的, 他作为中间结构, 在 tty 操作过程中, 通过这个结构体可以找到任何我们想要的 tty 相关的数据结构. 当 count
引用计数为 0 时被销毁.
线路规程中 N_TTY
中的 N 是 NEW 的意思, 新的线路规程, 当前默认的线路规程. 对应的还有 O_TTY
, 也就是 old tty. pty 设备用的就是 o_tty.
2、write
应用程序调用 write 会回调内核的 tty_wrte, 调用流程如下.
1 2 3 4 5 6 tty_write ()--> ld = tty_ldisc_ref_wait (tty)--> do_tty_write (ld->ops->write, tty, file, buf, count)--> ld->ops->write (tty, file, tty->write_buf, size)--> n_tty_write (tty, file, tty->write_buf, size)--> tty->ops->write (tty, b, nr)
write 的流程相对比较简单, 基本就是一路把 buffer 传下去. 果设置了 OPOST,输出字符会经过处理后再发送。如果未设置 OPOST
,则字符会原样发送到终端或设备,不经过任何特殊处理. 常见的处理包括将换行符 \n
自动转换为回车符 \r
和换行符 \n
组合(CR + NL)
,这是大多数终端默认的行为.
3、read
应用程序调用 read 会回调内核的 tty_read. read 的过程总共分为两步, 读取数据 、数据返回 .
1. 读取数据
这段代码比较长, 分段描述.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 static ssize_t n_tty_read (struct tty_struct *tty, struct file *file, unsigned char __user *buf, size_t nr) { struct n_tty_data *ldata = tty->disc_data; unsigned char __user *b = buf; DEFINE_WAIT_FUNC (wait, woken_wake_function); int c; int minimum, time; ssize_t retval = 0 ; long timeout; int packet; size_t tail; c = job_control (tty, file); if (c < 0 ) return c; if (file->f_flags & O_NONBLOCK) { if (!mutex_trylock (&ldata->atomic_read_lock)) return -EAGAIN; } else { if (mutex_lock_interruptible (&ldata->atomic_read_lock)) return -ERESTARTSYS; } down_read (&tty->termios_rwsem);
这段代码就是一些类型检查
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 minimum = time = 0 ; timeout = MAX_SCHEDULE_TIMEOUT; if (!ldata->icanon) { minimum = MIN_CHAR (tty); if (minimum) { time = (HZ / 10 ) * TIME_CHAR (tty); if (time) ldata->minimum_to_wake = 1 ; else if (!waitqueue_active (&tty->read_wait) || (ldata->minimum_to_wake > minimum)) ldata->minimum_to_wake = minimum; } else { timeout = (HZ / 10 ) * TIME_CHAR (tty); ldata->minimum_to_wake = minimum = 1 ; } } packet = tty->packet; tail = ldata->read_tail; add_wait_queue (&tty->read_wait, &wait);
这段代码会根据原始模式 还是规范模式 设置唤醒条件
原始模式 :按行缓冲输入,并处理特殊字符,适合需要行编辑的场景(如命令行)
规范模式 :不缓冲输入,字符立即传递,无特殊字符处理,适合实时响应输入的应用.
默认情况下无论是原始模式还是规范模式都是一样的设置. 即
1 2 timeout = (HZ / 10 ) * TIME_CHAR (tty); ldata->minimum_to_wake = 1 ;
表示只要有超过一个字符未被读取则唤醒读取操作.
TIME_CHAR(tty) 的值一般是 0 , 字符间的时间间隔超时为 0
原始模式的 minimum = tty->termios.c_cc[VMIN]
, 而规范模式 minimum = 0
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 while (nr) { if (packet && tty->link->ctrl_status) { unsigned char cs; if (b != buf) break ; spin_lock_irq (&tty->link->ctrl_lock); cs = tty->link->ctrl_status; tty->link->ctrl_status = 0 ; spin_unlock_irq (&tty->link->ctrl_lock); if (tty_put_user (tty, cs, b++)) { retval = -EFAULT; b--; break ; } nr--; break ; }
在 packet 模式下,如果链路状态发生了变化,则将链路状态 tty->link->ctrl_status
(例如:连接状态、信号状态等)作为数据返回给用户进程。这个用的也比较少,可以不用太过关注.
1 2 3 if (((minimum - (b - buf)) < ldata->minimum_to_wake) && ((minimum - (b - buf)) >= 1 )) ldata->minimum_to_wake = (minimum - (b - buf));
这段代码的目的是根据当前已经读取的数据量来调整最小唤醒字符数 minimum_to_wake
. 如果进程已经读取了一些数据,且剩余要读取的数据量 (minimum - (b - buf)
少于最初设定的唤醒条件,代码会更新 minimum_to_wake
,确保进程在读取到剩余数据后立即唤醒.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 if (!input_available_p (tty, 0 )) { up_read (&tty->termios_rwsem); tty_buffer_flush_work (tty->port); down_read (&tty->termios_rwsem); if (!input_available_p (tty, 0 )) { if (test_bit (TTY_OTHER_CLOSED, &tty->flags)) { retval = -EIO; break ; } if (tty_hung_up_p (file)) break ; if (!timeout) break ; if (file->f_flags & O_NONBLOCK) { retval = -EAGAIN; break ; } if (signal_pending (current)) { retval = -ERESTARTSYS; break ; } up_read (&tty->termios_rwsem); timeout = wait_woken (&wait, TASK_INTERRUPTIBLE, timeout); down_read (&tty->termios_rwsem); continue ; } }
input_available_p()
这个函数用来检测是否还有数据可以读. 返回1 表示有足够的数据可以读 , 返回 0 则表示没有足够的数据
规范模式下 :只有当 canon_head
和 read_tail
不相等时(即有完整的一行数据),才返回有数据可读。
原始模式下 :检查已经提交的数据量是否满足读取的最小要求,通常是 MIN_CHAR(tty),如果满足,则返回有数据可读。
进程读取数据的时候如果 n_tty_data 中没有数据,进程会在 TTY 关闭、挂起、超时、非阻塞模式 或 信号中断时退出读取操作 .
如果 n_tty_data 数据不足且允许阻塞,进程会进入休眠等待数据或超时 .
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 if (ldata->icanon && !L_EXTPROC (tty)) { retval = canon_copy_from_read_buf (tty, &b, &nr); if (retval) break ; } else { int uncopied; if (packet && b == buf) { if (tty_put_user (tty, TIOCPKT_DATA, b++)) { retval = -EFAULT; b--; break ; } nr--; } uncopied = copy_from_read_buf (tty, &b, &nr); uncopied += copy_from_read_buf (tty, &b, &nr); if (uncopied) { retval = -EFAULT; break ; } } n_tty_check_unthrottle (tty); if (b - buf >= minimum) break ; if (time) timeout = time; }
根据原始模式 还是规范模式 调用不同的接口读取数据, 读取的数据大于等于 minimum
返回.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 if (tail != ldata->read_tail) n_tty_kick_worker (tty); up_read (&tty->termios_rwsem); remove_wait_queue (&tty->read_wait, &wait); if (!waitqueue_active (&tty->read_wait)) ldata->minimum_to_wake = minimum; mutex_unlock (&ldata->atomic_read_lock); if (b - buf) retval = b - buf; return retval; }
这段代码是返回时的处理不再赘述.
2. 数据返回
从前面可以知道数据返回就是填充数据到 n_tty_data , 并且唤醒前面的 wait 进程返回数据. 对应接口 tty_flip_buffer_push
.
1 2 3 4 5 6 7 8 tty_flip_buffer_push ()--> tty_schedule_flip (port)--> queue_work (system_unbound_wq, &buf->work)--> flush_to_ldisc ()--> receive_buf (tty, head, count)--> disc->ops->receive_buf2 ()--> n_tty_receive_buf_common (tty, cp, fp, count, 1 ); --> __receive_buf()-->
在 __receive_buf 里面会根据 icanon 和 real_raw 决定数据拷贝的方式, 他们由 termios
三个参数设置, 组合如下.
ICANON
ECHO
ISIG
模式描述
行为
备注
1
1
1
规范模式,启用回显和信号处理
输入按行缓冲,输入的字符显示在屏幕上,支持信号处理(如 Ctrl+C
发送 SIGINT
信号中断程序)。
规范模式 (ICANON = 1, REAL_RAW = 0
)
1
1
0
规范模式,启用回显,禁用信号处理
输入按行缓冲,字符显示在屏幕上,但 Ctrl+C
等信号无法中断程序。
规范模式 (ICANON = 1, REAL_RAW = 0
)
1
0
1
规范模式,禁用回显,启用信号处理
输入按行缓冲,输入的字符不会显示在屏幕上,但支持 Ctrl+C
等信号中断功能。
规范模式 (ICANON = 1, REAL_RAW = 0
)
1
0
0
规范模式,禁用回显和信号处理
输入按行缓冲,输入的字符不会显示在屏幕上,且 Ctrl+C
等信号无效。
规范模式 (ICANON = 1, REAL_RAW = 0
)
0
1
1
非规范模式,启用回显和信号处理
输入字符立即传递,不需要按回车,字符显示在屏幕上,且支持 Ctrl+C
信号中断功能。
非规范模式 (ICANON = 0, REAL_RAW = 0
)
0
1
0
非规范模式,启用回显,禁用信号处理
输入字符立即传递,字符显示在屏幕上,但信号处理功能(如 Ctrl+C
)无效。
非规范模式 (ICANON = 0, REAL_RAW = 0
)
0
0
1
非规范模式,禁用回显,启用信号处理
输入字符立即传递,字符不显示在屏幕上,但支持 Ctrl+C
信号中断功能。
部分原始模式 (ICANON = 0, REAL_RAW = 0
)
0
0
0
非规范模式,禁用回显和信号处理(原始模式)
输入字符立即传递,字符不显示在屏幕上,且 Ctrl+C
等信号无效。
原始模式 (ICANON = 0, REAL_RAW = 1
)
3. 常见的 termios
termios 就是控制线路规程的规则, 一般驱动设置一个默认值 tty_std_termios
, 应用可以根据自己的需要做修改.
标志类别
标志名
含义描述
输入标志 (`c_iflag`)
ICRNL
将回车 (`CR`) 转换为换行 (`NL`)。
IXON
启用软件流控制(XON/XOFF)。
输出标志 (`c_oflag`)
OPOST
启用输出处理。关闭时输出字符直接发送,不做任何处理。
ONLCR
将输出的换行符 (`NL`) 转换为回车符和换行符组合 (`CR-NL`)。
控制标志 (`c_cflag`)
B38400
设置波特率为 38400 bps。
CS8
设置字符大小为 8 位。
CREAD
启用接收器,允许接收数据。
HUPCL
在最后一个进程关闭时挂断连接,释放串口。
本地标志 (`c_lflag`)
ISIG
启用信号处理(例如 `Ctrl+C` 发送 `SIGINT` 信号)。
ICANON
启用规范模式(行缓冲模式),输入会被缓冲直到按下换行符或其他结束符。
ECHO
启用回显,用户输入的字符会显示在屏幕上。
ECHOE
退格时删除前一个字符,并在屏幕上同步删除。
ECHOK
在输入 `KILL` 字符时删除整行,并显示换行。
ECHOCTL
回显控制字符(例如 `Ctrl+C` 显示为 `^C`)。
ECHOKE
删除整行时回显该操作。
扩展标志 (`c_lflag`)
IEXTEN
启用实现的扩展输入处理(例如终端的额外特性)。
四、编程实验
有了前面的知识, 我们就来自己写一个我们的终端驱动程序. 这个终端的功能很简单就是输入字符立即传递 , 但是需要能够响应 crtl + c 等操作 . 因此就是 ICANON = 0, REAL_RAW = 0(非规范模式) . 驱动程序如下.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 #include <linux/module.h> #include <linux/tty.h> #include <linux/tty_flip.h> #include <linux/init.h> #include <linux/tty_driver.h> #include <linux/slab.h> #define DRIVER_NAME "my_tty" #define TTY_DRIVER_NAME "ttyMY" #define TTY_MINORS 1 struct my_tty { struct tty_port *port; struct tty_driver *driver; struct device *dev; }; static struct my_tty *my_tty;static int my_tty_open (struct tty_struct *tty, struct file *filp) { printk ("my_tty: Device opened\n" ); return tty_port_open (tty->port, tty, filp); } static void my_tty_close (struct tty_struct *tty, struct file *filp) { printk ("my_tty: Device close, tty->count: %d\n" , tty->count); tty_port_close (tty->port, tty, filp); } static int my_tty_write (struct tty_struct *tty, const unsigned char *buf, int count) { int i; for (i = 0 ; i < count; i++) { printk ("%c" , buf[i]); tty_insert_flip_char (tty->port, buf[i], TTY_NORMAL); } printk ("\n%s count:%d \n" , __func__, count); if (count) tty_flip_buffer_push (tty->port); return count; } static int my_tty_write_room (struct tty_struct *tty) { if (tty->stopped) return 0 ; return tty_buffer_space_avail (tty->port); } static const struct tty_operations my_tty_ops = { .open = my_tty_open, .close = my_tty_close, .write = my_tty_write, .write_room = my_tty_write_room, }; static const struct tty_port_operations null_ops = { }; static int my_tty_driver_register (void ) { int retval; my_tty = kzalloc (sizeof (*my_tty), GFP_KERNEL); if (!my_tty) return -ENOMEM; my_tty->port = kzalloc (sizeof (*my_tty->port), GFP_KERNEL); if (!my_tty->port) { kfree (my_tty); return -ENOMEM; } my_tty->driver = tty_alloc_driver (TTY_MINORS, TTY_DRIVER_RESET_TERMIOS); if (!my_tty->driver) { kfree (my_tty->port); kfree (my_tty); return -ENOMEM; } my_tty->driver->driver_name = DRIVER_NAME; my_tty->driver->name = TTY_DRIVER_NAME; my_tty->driver->major = 0 ; my_tty->driver->type = TTY_DRIVER_TYPE_SERIAL; my_tty->driver->subtype = SERIAL_TYPE_NORMAL; my_tty->driver->flags = TTY_DRIVER_REAL_RAW | TTY_DRIVER_DYNAMIC_DEV; my_tty->driver->init_termios = tty_std_termios; my_tty->driver->init_termios.c_cflag = B9600 | CS8 | CREAD | HUPCL | CLOCAL; my_tty->driver->init_termios.c_ispeed = 9600 ; my_tty->driver->init_termios.c_ospeed = 9600 ; tty_set_operations (my_tty->driver, &my_tty_ops); retval = tty_register_driver (my_tty->driver); if (retval) { put_tty_driver (my_tty->driver); kfree (my_tty->port); kfree (my_tty); return retval; } tty_port_init (my_tty->port); my_tty->port->ops = &null_ops; my_tty->driver->ports[0 ] = my_tty->port; my_tty->dev = tty_register_device (my_tty->driver, 0 , NULL ); if (IS_ERR (my_tty->dev)) { retval = PTR_ERR (my_tty->dev); tty_unregister_driver (my_tty->driver); put_tty_driver (my_tty->driver); kfree (my_tty->port); kfree (my_tty); return retval; } printk ("My TTY driver loaded. Major number: %d\n" , my_tty->driver->major); return 0 ; } static void my_tty_driver_unregister (void ) { tty_unregister_driver (my_tty->driver); put_tty_driver (my_tty->driver); tty_port_destroy (my_tty->port); kfree (my_tty->port); kfree (my_tty); printk ("My TTY driver unloaded.\n" ); } static int __init my_tty_init (void ) { return my_tty_driver_register (); } static void __exit my_tty_exit (void ) { my_tty_driver_unregister (); } module_init (my_tty_init);module_exit (my_tty_exit);MODULE_LICENSE ("GPL" );MODULE_AUTHOR ("baron" ); MODULE_DESCRIPTION ("a simple tty driver" );
应用程序编写如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 #include <stdlib.h> #include <fcntl.h> #include <unistd.h> #include <string.h> #include <errno.h> #include <stdio.h> #include <termios.h> #define DEVICE "/dev/ttyMY0" #define BUFFER_SIZE 1024 int main () { int fd; char write_buf[] = "Hello, this is a test from user space!" ; char read_buf[BUFFER_SIZE]; int bytes_written, bytes_read; struct termios tty; fd = open (DEVICE, O_RDWR | O_NOCTTY); if (fd < 0 ) { perror ("Failed to open the device" ); return errno; } printf ("Opened device: %s\n" , DEVICE); if (tcgetattr (fd, &tty) != 0 ) { perror ("Failed to get terminal attributes" ); close (fd); return errno; } tty.c_lflag &= ~ICANON; tty.c_lflag &= ~ECHO; tty.c_lflag |= ISIG; if (tcsetattr (fd, TCSANOW, &tty) != 0 ) { perror ("Failed to set terminal attributes" ); close (fd); return errno; } printf ("Writing to the device: %s\n" , write_buf); bytes_written = write (fd, write_buf, strlen (write_buf)); if (bytes_written < 0 ) { perror ("Failed to write to the device" ); close (fd); return errno; } printf ("Wrote %d bytes to the device\n" , bytes_written); printf ("Reading from the device...\n" ); bytes_read = read (fd, read_buf, BUFFER_SIZE); if (bytes_read < 0 ) { perror ("Failed to read from the device" ); close (fd); return errno; } printf ("Read %d bytes: %.*s\n" , bytes_read, bytes_read, read_buf); close (fd); printf ("Closed device: %s\n" , DEVICE); return 0 ; }
验证结果:
1 2 3 4 5 6 7 # tty_test Opened device: /dev/ttyMY0 Writing to the device: Hello, this is a test from user space! Wrote 38 bytes to the device Reading from the device... Read 38 bytes: Hello, this is a test from user space! Closed device: /dev/ttyMY0