admin 2025-07-07
153
tty这个名称源于电传打字节的简称,在linux表示各种终端,终端通常都跟硬件相对应。比如对应于输入设备键盘鼠标,输出设备显示器的控制终端和串口终端。也有对应于不存在设备的pty驱动。在如此众多的终端模型之中,linux是怎么将它们统一建模的呢?这就是我们今天要讨论的问题。
tty驱动概貌tty架构如下所示:
如上图所示,用户空间主要是通过系统调用与ttycore交互。ttycore根据用空间操作的类型再选择跟linediscipline和ttydriver交互。
例如,设置硬件的ioctl指令就直接交给tty_driver处理。read和write操作就会交给linediscipline处理。
Linediscipline是线路规程的意思。正如它的名字一样,它表示的是这条终端”线程”的输入与输出规范设置。主要用来进行输入/输出数据的预处理。
处理之后,就会将数据交给ttydriver,它将字符转换成终端可以理解的字串。将其传给终端设备。
值得注意的是,这个架构没有为ttydriver提供read操作。也就是说ttycore和linediscipline都没有办法从ttydriver里直接读终端信息。这是因为ttydriver对应的hardware并不一定是输入数据和输出数据的共同负载者。
例如控制终端,输出设备是显示器,输入设备是键盘。基于这样的原理。在linediscipline中有一个输入缓存区,并提供了一个名叫receive_buf()的接口函数。对应的终端设备只要调用linediscipine的receiver_buf函数,将数据写入到输入缓存区就可以了。如果一个设备同时是输入设备又是输出设备。那在设备的中断处理中调用receive_buf()将数据写入即可.
tty驱动接口分析tty_init()/**Ok,nowwecaninitializetherestofthettydevicesandcancount*onmemoryallocations,interruptsetc..*/int__inittty_init(void){tty_sysctl_init();cdev_init(tty_cdev,tty_fops);if(cdev_add(tty_cdev,MKDEV(TTYAUX_MAJOR,0),1)||register_chrdev_region(MKDEV(TTYAUX_MAJOR,0),1,"/dev/tty")0)panic("Couldn'tregister/dev/ttydriver\n");device_create(tty_class,NULL,MKDEV(TTYAUX_MAJOR,0),NULL,"tty");cdev_init(console_cdev,console_fops);if(cdev_add(console_cdev,MKDEV(TTYAUX_MAJOR,1),1)||register_chrdev_region(MKDEV(TTYAUX_MAJOR,1),1,"/dev/console")0)panic("Couldn'tregister/dev/consoledriver\n");consdev=device_create_with_groups(tty_class,NULL,MKDEV(TTYAUX_MAJOR,1),NULL,cons_dev_groups,"console");if(IS_ERR(consdev))consdev=NULL;ifreturn0;}tty_init主要做了以下工作:
初始化tty子系统的sysctl相关设置,包括注册sysctl参数、创建sysctl目录等。
初始化tty设备的字符设备对象,并将其与tty设备操作函数tty_fops绑定。同时,创建一个名为"tty"的tty设备节点,并将其设备号设置为MKDEV(TTYAUX_MAJOR,0)。
初始化控制台设备的字符设备对象,并将其添加到字符设备系统中。同时,创建一个名为"console"的控制台设备节点,并将其设备号设置为MKDEV(TTYAUX_MAJOR,1)。该控制台设备节点还将在sysfs中创建一个名为"console"的目录,并在该目录下创建多个属性文件,用于控制控制台的一些属性。
如果内核支持虚拟终端,则初始化虚拟终端。
tty_alloc_driver/*UseTTY_DRIVER_*flagsbelow*/#definetty_alloc_driver(lines,flags)\__tty_alloc_driver(lines,THIS_MODULE,flags)
__tty_alloc_driver()用于分配一个tty驱动程序的数据结构structtty_driver,并对其一些常用字段进行初始化。
/***__tty_alloc_driver--allocatettydriver*@lines:countoflinesthisdrivercanhandleatmost*@owner:modulewhichisrepsonsibleforthisdriver*@flags:someofTTY_DRIVER_*flags,willbesetindriver-flags**Thisshouldnotbecalleddirectly,someoftheprovidedmacrosshouldbe*_ERRandfrison@retval.*/structtty_driver*__tty_alloc_driver(unsignedintlines,structmodule*owner,unsignedlongflags){structtty_driver*driver;unsignedintcdevs=1;interr;if(!lines||(flagsTTY_DRIVER_UNNUMBERED_NODElines1))returnERR_PTR(-EINVAL);/*分配一个structtty_driver结构体,并对其中的一些字段进行初始化,包括num、owner、flags等*/driver=kzalloc(sizeof(structtty_driver),GFP_KERNEL);if(!driver)returnERR_PTR(-ENOMEM);kref_init(driver-kref);driver-magic=TTY_DRIVER_MAGIC;driver-num=lines;driver-owner=owner;driver-flags=flags;/*如果TTY_DRIVER_DEVPTS_MEM标志位没有被设置,那么函数会分配driver-ttys和driver-termios,否则不需要分配*/if(!(flagsTTY_DRIVER_DEVPTS_MEM)){driver-ttys=kcalloc(lines,sizeof(*driver-ttys),GFP_KERNEL);driver-termios=kcalloc(lines,sizeof(*driver-termios),GFP_KERNEL);if(!driver-ttys||!driver-termios){err=-ENOMEM;gotoerr_free_all;}}/*如果TTY_DRIVER_DYNAMIC_ALLOC标志位没有被设置,那么函数会分配driver-ports,否则不需要分配*/if(!(flagsTTY_DRIVER_DYNAMIC_ALLOC)){driver-ports=kcalloc(lines,sizeof(*driver-ports),GFP_KERNEL);if(!driver-ports){err=-ENOMEM;gotoerr_free_all;}cdevs=lines;}/*函数会根据lines的值分配相应数量的driver-cdevs*/driver-cdevs=kcalloc(cdevs,sizeof(*driver-cdevs),GFP_KERNEL);if(!driver-cdevs){err=-ENOMEM;gotoerr_free_all;}returndriver;err_free_all:kfree(driver-ports);kfree(driver-ttys);kfree(driver-termios);kfree(driver-cdevs);kfree(driver);returnERR_PTR(err);}tty_register_drivertty_register_driver用于注册tty驱动程序的,被tty驱动程序调用以将自己注册到内核中。
/**Calledbyattydrivertoregisteritself.*/inttty_register_driver(structtty_driver*driver){interror;inti;dev_tdev;structdevice*d;/*确认是否要内核动态分配主设备号*/if(!driver-major){/*函数调用alloc_chrdev_region函数来动态分配主设备号,并将分配的主设备号和次设备号保存在driver-major和driver-minor_start字段中*/error=alloc_chrdev_region(dev,driver-minor_start,driver-num,driver-name);if(!error){driver-major=MAJOR(dev);driver-minor_start=MINOR(dev);}}else{/*已经预先分配了主设备号,函数调用register_chrdev_region函数来注册设备号*/dev=MKDEV(driver-major,driver-minor_start);error=register_chrdev_region(dev,driver-num,driver-name);}if(error0)gotoerr;/*判断是否设置了TTY_DRIVER_DYNAMIC_ALLOC标志位*/if(driver-flagsTTY_DRIVER_DYNAMIC_ALLOC){/*需要动态分配tty设备号,函数调用tty_cdev_add函数来添加tty设备号,并将每个tty设备的字符设备注册到内核中*/error=tty_cdev_add(driver,dev,0,driver-num);if(error)gotoerr_unreg_char;}mutex_lock(tty_mutex);/*将driver添加到链表tty_drivers中*/list_add(driver-tty_drivers,tty_drivers);mutex_unlock(tty_mutex);/*判断TTY_DRIVER_DYNAMIC_DEV标志位是否设置*/if(!(driver-flagsTTY_DRIVER_DYNAMIC_DEV)){for(i=0;idriver-num;i++){/*需要注册固定的tty设备号,函数在循环中调用tty_register_device函数来注册每个tty设备号,并将每个tty设备注册到内核中*/d=tty_register_device(driver,i,NULL);if(IS_ERR(d)){error=PTR_ERR(d);gotoerr_unreg_devs;}}}/*注册/proc/tty/drivers目录中的信息*/proc_tty_register_driver(driver);/*将driver结构体中的flags字段设置为TTY_DRIVER_INSTALLED,表示该驱动程序已经被成功注册到内核中*/driver-flags|=TTY_DRIVER_INSTALLED;return0;err_unreg_devs:for(i--;i=0;i--)tty_unregister_device(driver,i);mutex_lock(tty_mutex);list_del(driver-tty_drivers);mutex_unlock(tty_mutex);err_unreg_char:unregister_chrdev_region(dev,driver-num);err:returnerror;}tty_register_driver()函数操作比较简单。就是为tty_driver创建字符设备。然后将字符设备的操作集指定为tty_fops。并且将tty_driver挂载到tty_drivers链表中。这个链表中是以设备号为关键字找到对应的driver。
特别的。如果没有定义TTY_DRIVER_DYNAMIC_DEV。还会在sysfs中创建一个类设备。这样主要是为了udev管理设备。
tty_unregister_devicetty_unregister_device用于注销一个tty设备。该函数的作用是销毁设备节点和字符设备,以便于释放与该tty设备相关的资源,例如内存和设备文件等.
/***tty_unregister_device-unregisterattydevice*@driver:thettydriverthatdescribesthettydevice*@index:theindexinthettydriverforthisttydevice**Ifattydeviceisregisteredwithacalltotty_register_device()then*thisfunctionmustbecalledwhenthettydeviceisgone.**Locking:??*/voidtty_unregister_device(structtty_driver*driver,unsignedindex){device_destroy(tty_class,MKDEV(driver-major,driver-minor_start)+index);if(!(driver-flagsTTY_DRIVER_DYNAMIC_ALLOC)){cdev_del(driver-cdevs[index]);driver-cdevs[index]=NULL;}}tty_unregister_device所做工作如下:
调用device_destroy函数来销毁tty设备对应的设备节点。接受两个参数:第一个参数tty_class表示tty类,第二个参数是tty设备的设备号,其中MKDEV(driver-major,driver-minor_start)+index表示tty设备的设备号,driver-major表示tty设备的主设备号,driver-minor_start表示tty设备的次设备号的起始值,index表示tty设备的索引
如果该tty驱动程序不是动态分配的,则调用cdev_del函数来注销该tty设备对应的字符设备。
get_tty_driverget_tty_driver作用是在用户空间的应用程序使用tty设备时,获取对应的tty驱动程序的信息。
/***get_tty_driver-finddeviceofatty*@dev_t:deviceidentifier*@index:returnstheindexofthetty**Thisroutinereturnsattydriverstructure,givenadevicenumber*andalsopassesbacktheindexnumber.**Locking:callermustholdtty_mutex*/staticstructtty_driver*get_tty_driver(dev_tdevice,int*index){structtty_driver*p;/**/list_for_each_entry(p,tty_drivers,tty_drivers){dev_tbase=MKDEV(p-major,p-minor_start);if(devicebase||device=base+p-num)continue;*index=device-base;returntty_driver_kref_get(p);}returnNULL;}首先使用list_for_each_entry循环遍历全局链表tty_drivers,该链表中保存了所有已经注册的tty驱动程序。对于每个tty驱动程序,函数将其设备号的起始值和结束值计算出来,如果给定设备号不在这个范围内,则继续遍历下一个tty驱动程序。
如果给定设备号在某个tty驱动程序的范围内,则计算出该设备号对应的tty设备的索引值,并调用tty_driver_kref_get函数来获取该tty驱动程序的引用计数。函数返回该tty驱动程序的结构体指针,并将找到的tty设备的索引值保存到index参数中。
需要注意的是,函数在访问全局链表tty_drivers时,需要持有互斥锁tty_mutex。因为多个应用程序可能同时访问同一个tty驱动程序,如果没有互斥锁保护,可能会导致并发问题。
tty_open从注册的过程可以看到,所有的操作都会对应到tty_fops中。Open操作对应的操作接口是tty_open(),用于打开一个tty设备。函数的作用是在用户空间的应用程序使用tty设备时,打开对应的tty设备,并初始化相应的数据结构。
/***tty_open-openattydevice*@inode:inodeofdevicefile*@filp:filepointertotty**tty_openandtty_releasekeepupthettycountthatcontainsthe*,as*differentinodesmightpointtothesametty.**Open-countingisneededforptymasters,aswellasforkeeping*trackofseriallines:DTRisdroppedwhenthelastclosehappens.*(Thisisnotdonesolelythroughtty-count,now.-Ted1/27/92)**Thetermiosstateofaptyisresetonfirstopensothat*settingsdon'tpersistacrossreuse.**Locking:tty_mutexprotectstty,tty_lookup_driverandtty_init_dev.*tty-countshouldprotecttherest.*-siglockprotects-signal/-sighand**Note:thetty_unlock/lockcaseswithoutarefareonlysafedueto*tty_mutex*/staticinttty_open(structinode*inode,structfile*filp){structtty_struct*tty;intnoctty,retval;structtty_driver*driver=NULL;intindex;dev_tdevice=inode-i_rdev;unsignedsaved_flags=filp-f_flags;nonseekable_open(inode,filp);retry_open:/*分配一个tty结构体*/retval=tty_alloc_file(filp);if(retval)return-ENOMEM;/*检查文件的标志位,如果包含O_NOCTTY标志,则禁止将该tty设备设置为控制终端*/noctty=filp-f_flagsO_NOCTTY;index=-1;retval=0;/*尝试打开当前的tty设备*/tty=tty_open_current_tty(device,filp);if(!tty){mutex_lock(tty_mutex);/*根据设备号来查找对应的tty驱动程序,并初始化该tty设备,将找到的tty驱动程序保存到driver变量中*/driver=tty_lookup_driver(device,filp,noctty,index);if(IS_ERR(driver)){retval=PTR_ERR(driver);gotoerr_unlock;}/*checkwhetherwe'rereopeninganexistingtty*//*查找对应的tty设备,并将找到的tty设备结构体指针保存到tty变量中*/tty=tty_driver_lookup_tty(driver,inode,index);if(IS_ERR(tty)){retval=PTR_ERR(tty);gotoerr_unlock;}if(tty){/*如果找到了该tty设备,则需要重新打开该tty设备*/mutex_unlock(tty_mutex);retval=tty_lock_interruptible(tty);tty_kref_put(tty);/*dropkreffromtty_driver_lookup_tty()*/if(retval){if(retval==-EINTR)retval=-ERESTARTSYS;gotoerr_unref;}retval=tty_reopen(tty);if(retval0){tty_unlock(tty);tty=ERR_PTR(retval);}}else{/*Returnswiththetty_lockheldfornow*//*需要初始化该tty设备*/tty=tty_init_dev(driver,index);/*为该tty设备分配一个tty结构体,并对其进行初始化*/mutex_unlock(tty_mutex);}tty_driver_kref_put(driver);}if(IS_ERR(tty)){retval=PTR_ERR(tty);if(retval!=-EAGAIN||signal_ping(current))gotoerr_file;tty_free_file(filp);schedule();gotoretry_open;}/*将该tty设备与文件结构体相关联*/tty_add_file(tty,filp);check_tty_count(tty,__func__);/*如果该tty设备是一个伪终端主设备,则需要将noctty标志设置为1*/if(tty-driver-type==TTY_DRIVER_TYPE_PTYtty-driver-subtype==PTY_TYPE_MASTER)noctty=1;tty_debug_hangup(tty,"(ttycount=%d)\n",tty-count);/*调用tty设备的open函数*/if(tty-ops-open)retval=tty-ops-open(tty,filp);elseretval=-ENODEV;filp-f_flags=saved_flags;if(retval){tty_debug_hangup(tty,"error%d,releasing\n",retval);tty_unlock(tty);/*needtocalltty_releasewithoutBTM*/tty_release(inode,filp);if(retval!=-ERESTARTSYS)returnretval;if(signal_ping(current))returnretval;schedule();/**Needtoresetf_opincaseahanguphappened.*/if(tty_hung_up_p(filp))filp-f_op=tty_fops;gotoretry_open;}clear_bit(TTY_HUPPED,tty-flags);read_lock(tasklist_lock);spin_lock_irq(¤t-sighand-siglock);if(!nocttycurrent-signal-leader!current-signal-ttytty-session==NULL){/**Don'tletaprocessthatonlyhaswriteaccesstothetty*obtaintheprivilegesassociatedwithhavingattyas*controllingterminal(beingabletoreopenitwithfull*accessthrough/dev/tty,beingabletoperformpushback).*Manydistributionssetthegroupofallttysto"tty"and*grantwrite-onlyaccesstoallterminalsforsetgidtty*binaries,whichshouldnotimplyfullprivilegesonallttys.**Thiscouldtheoreticallybreakoldcodethatperformsopen()*,itmightbe*necessarytoalsopermitthisif*inode_permission(inode,MAY_READ)==0.*/if(filp-f_modeFMODE_READ)__proc_set_tty(tty);}spin_unlock_irq(¤t-sighand-siglock);read_unlock(tasklist_lock);tty_unlock(tty);return0;err_unlock:mutex_unlock(tty_mutex);err_unref:/*afterlockstoavoiddeadlock*/if(!IS_ERR_OR_NULL(driver))tty_driver_kref_put(driver);err_file:tty_free_file(filp);returnretval;}函数所作工作如下:
在打开tty设备时,该函数会检查文件的标志位,如果包含O_NOCTTY标志,则禁止将该tty设备设置为控制终端。这是因为如果一个进程打开一个tty设备并将其设置为控制终端,其他进程就无法再将该tty设备设置为控制终端,这可能会导致一些问题。
如果打开当前的tty设备失败,则需要根据设备号来查找对应的tty驱动程序,并初始化该tty设备。在查找tty驱动程序时,需要调用tty_lookup_driver函数来查找对应的tty驱动程序,并将找到的tty驱动程序保存到driver变量中。如果找不到对应的tty驱动程序,则返回错误码。
如果找到了对应的tty驱动程序,则调用tty_driver_lookup_tty函数来查找对应的tty设备,并将找到的tty设备结构体指针保存到tty变量中。如果找到了该tty设备,则需要重新打开该tty设备。否则,需要初始化该tty设备。在初始化tty设备时,需要调用tty_init_dev函数来为该tty设备分配一个tty结构体,并对其进行初始化。
在打开tty设备之后,函数会调用tty_add_file函数将该tty设备与文件结构体相关联。此外,如果该tty设备是一个伪终端主设备,则需要将noctty标志设置为1。
最后,函数会调用tty设备的open函数,如果存在的话,来进行一些特定的操作。如果open函数返回错误码,则需要释放该tty设备并返回错误码。如果open函数返回-ERESTARTSYS,则需要重新打开该tty设备。如果有中断发生,也需要重新打开该tty设备。
tty_writetty_write()作用是将用户数据写入tty设备,并通过线路规则(linediscipline)进行处理。
线路规则是tty设备的一种机制,用于处理和转换从用户进程到内核和设备的数据流。在写入tty设备之前,需要获取该tty设备的线路规则,并调用其write方法进行处理。
/***tty_write-writemethodforttydevicefile*@file:ttyfilepointer*@buf:userdatatowrite*@count:bytestowrite*@ppos:unused**Writedatatoattydeviceviathelinediscipline.**Locking:*Locksthelinedisciplineasrequired*Writestothettydriverareserializedbytheatomic_write_lock**writemethodwillnotbeinvokedinparallelforeachdevice.*/staticssize_ttty_write(structfile*file,constchar__user*buf,size_tcount,loff_t*ppos){structtty_struct*tty=file_tty(file);structtty_ldisc*ld;ssize_tret;if(tty_paranoia_check(tty,file_inode(file),"tty_write"))return-EIO;if(!tty||!tty-ops-write||(test_bit(TTY_IO_ERROR,tty-flags)))return-EIO;/*Shorttermdebugtocatchbuggydrivers*/if(tty-ops-write_room==NULL)printk(KERN_ERR"ttydriver%slacksawrite_roommethod.\n",tty-driver-name);ld=tty_ldisc_ref_wait(tty);if(!ld-ops-write)ret=-EIO;elseret=do_tty_write(ld-ops-write,tty,file,buf,count);tty_ldisc_deref(ld);returnret;}tty_write()所作工作如下:
首先从文件指针中获取tty_struct数据结构的指针,表示要写入的tty设备。
检查传入的tty_struct指针是否有效,以及是否有其他进程正在访问该tty设备。如果出现问题,返回输入/输出错误码-EIO。
检查tty_struct指针是否有效、tty设备是否支持写操作,以及是否已经出现了输入/输出错误。如果出现问题,返回输入/输出错误码-EIO。
检查tty设备是否实现了write_room方法,如果没有,则输出错误信息。
获取tty设备的线路规则(linediscipline),并等待获取成功。
检查线路规则的write方法是否存在,如果不存在,返回输入/输出错误码-EIO。否则,调用do_tty_write函数,将数据写入tty设备。
释放线路规则引用计数器。
返回写入操作的结果,如果写入成功,则返回写入的字节数;否则,返回相应的错误码。
tty_read/***tty_read-readmethodforttydevicefiles*@file:pointertottyfile*@buf:userbuffer*@count:sizeofuserbuffer*@ppos:unused***forhungupdevicesbeforecallingthelinedisciplinemethod.**Locking:**readcallsmaybeoutstandinginparallel.*/staticssize_ttty_read(structfile*file,char__user*buf,size_tcount,loff_t*ppos){inti;structinode*inode=file_inode(file);structtty_struct*tty=file_tty(file);structtty_ldisc*ld;if(tty_paranoia_check(tty,inode,"tty_read"))return-EIO;if(!tty||(test_bit(TTY_IO_ERROR,tty-flags)))return-EIO;/*Wewanttowaitforthelinedisciplinetosortoutinthissituation*/ld=tty_ldisc_ref_wait(tty);if(ld-ops-read)i=ld-ops-read(tty,file,buf,count);elsei=-EIO;tty_ldisc_deref(ld);if(i0)tty_update_time(inode-i_atime);returni;}tty_read()实现终端设备文件读操作的函数。
获取tty_struct结构体、inode和linediscipline对象的指针。
调用tty_paranoia_check()函数检查tty_struct结构体是否可用。如果检查失败,返回-EIO。
检查tty_struct结构体是否为空或者TTY_IO_ERROR标志位已经设置。如果是,则返回-EIO。
获取linediscipline对象的引用,确保它不会在tty_read()函数执行期间被卸载。
检查linediscipline的read()方法是否可用。如果可用,则调用该方法进行读取操作,并将返回的字节数保存在变量i中。如果不可用,返回-EIO。
释放linediscipline的引用。
如果读取操作成功,调用tty_update_time()函数更新inode的访问时间。
返回读取的字节数。
小结在这一节里,只对tty的构造做一个分析,具体的比如线路规程的内容我们了解知道就好,这里不做深入分析。
本文参考