天天看點

SVR4/4.3BSD與Linux對待僞終端的不同方式

打開僞終端意味着打開了一個“終端對”,這個終端對的其中一個是主終端,另一個是從終端,簡單說主終端和類似sshd,telnetd等使用者空間的遠端協定處理程序連接配接,而從終端則和shell之類的實際程序連接配接,在處理遠端登入的時候,一般都是由遠端協定處理程序打開主終端和從終端,然後就在遠端網絡終端和本機shell之間建立了一條雙向通道--“遠端網絡終端-(套接字)--本機協定處理程序--主終端--從終端--shell”,在這個“打開主從終端建立連接配接”的語義以及其實作上,有着不同的标準,總的來說有三種方式,分别是SVR4的方式,BSD的方式以及linux的方式,在“建立連接配接”的語義上SVR4的方式使用“流”來建立這條連接配接,而BSD和linux則是自動建立的,在“打開主從終端”的語義上,SVR4和linux是自動确定主終端并打開主終端後自動确定從終端,而BSD則必須手工确定和打開主終端,可見linux處理僞終端的方式是結合SVR4和BSD兩種UNIX标準的結果,linux不僅實作這種有意義的最佳組合,而且分别實作了SRV和BSD的兩種方式的接口,如果編譯CONFIG_LEGACY_PTYS宏,則可以使用BSD的方式,如果編譯CONFIG_UNIX98_PTYS,則實作SRV4的接口。

     參見《unix環境進階程式設計》的第19章,在使用者空間,SVR4的方式為:

int ptym_open(char *pts_name)

{

    strcpy(pts_name, "/dev/ptmx"); //準備主終端的檔案名字

    fdm = open(pts_name, O_RDWR);  //打開主終端

    grantpt(fdm);  //連接配接次終端

    ptr = ptsname(fdm); //得到次終端的檔案名字

    strcpy(pts_name, ptr);

    return fdm;

}

int ptys_open(int fdm, char *pts_name)

    fds = open(pts_name, O_RDWR); //打開次終端

    ioctl(fds, I_PUSH, "ptem");   //壓入一個僞終端虛拟子產品

    ioctl(fds, I_PUSH, "ldterm"); //壓入行規程子產品

    return fds;

而BSD的方式卻是:

    ...

    //一個for循環,從/dev/ptyp0開始一直找到/dev/ptyTf為止,尋到第一個沒有被使用的作為主終端,然後将對應的檔案名的ptyXY中的p改為t,即ttyXY就是次終端

    pts_name[5] = 't';  ///dev/ptyXY中的第6個元素就是p,現在改為t

    //無需壓入流子產品,因為對于bsd來講,其驅動程式是基于寫死和clist的。

可見SRV4和BSD的方式根本不同,通過了解這種不同,我們也能更加明白/dev目錄下的關于終端檔案的命名規則了。關于流機制,BSD沒有實作,可能是由于BSD自最初就沒有SRV經過良好的規劃吧,加之流的作者直接貢獻流機制于SRV,那時unix已經分裂了。從終端的行規程以及任何終端的行規程之類其實也是一種協定,隻是該協定主要規定人-機界面的規則,之是以将之稱為行規程就是因為該規程在标準模式下限制了一次輸入的結束就是一個換行符,這就是一個行規成協定,畢竟機器并不知道何時人們會輸入完畢也就不能預先讀取特定大小的資料塊,而隻能硬性規定一個特殊的字元作為輸入結束,該特殊字元就是換行,類似tcp/ip協定,隻是沒有後者普遍罷了,使用壓入流的方式,你可以輕易的堆積一個協定棧,隻要将/dev/ip|udp|tcp等協定裝置檔案依次用ioctl的I_PUSH指令堆積即可。行規程壓入了從終端并沒有壓入主終端,可見主終端僅僅起到一個資料中轉的作用,主終端之是以不需要行規程,那是因為從終端可以處理直接從程序寫入的資料或者說可以處理資料邊界以及轉義問題,這是無關緊要的,完全可以重新實作一個僞終端驅動,然後在主從終端都壓入行規程子產品,這樣就顯得更加對稱了,正如linux後來實作的那樣,雖然linux并沒有顯式地實作流機制,在linux中的僞終端tty的write函數中,主從終端都是統一一緻的,如此一來在linux中,主從終端就更加像一對管道了,起碼比SRV的要對稱:

static int pty_write(...)

    struct tty_struct *to = tty->link; //直接擷取“一對的另一半”

    to->ldisc.receive_buf(to, temp_buffer, NULL, n);//将資料放入另一半的緩沖區

如果看一下linux實作pty的源碼,就會發現實際上pts使用的行規程是tty_ldisc_N_TTY,其receive_buf對資料其實并沒有做太複雜的加工,是以這條管道并不複雜。

     在打開一對終端方面,linux實作了兩種方式,SRV4的方式和BSD的方式,總之,實作這兩種接口是之前unix标準混戰的結果。以SRV4的方式為例,linux中使用了一個/dev/ptmx裝置檔案,該裝置檔案隻有一個卻可以集中代表所有的主終端,任何sshd,telnetd之類的程序都可以隻使用者一個終端裝置檔案,雖然裝置檔案是一個,但是由于核心中file資料結構是基于程序的,是以各個程序對該裝置檔案的引用卻可以容納不同的資料,包括不同的從終端。在ptmx_open中,不僅系統可以自動配置設定一個主終端,而且還為該主終端綁定了一個從終端,主終端設定到file結構體的private_data字段上,之後諸如sshd,telnetd之類的程序讀寫/dev/ptmx檔案時,雖然它們讀寫的是同一個檔案,可是由于file結構體不再它們之間共享,是以它們取到的file->private_data也就不同了:

static int ptmx_open(struct inode * inode, struct file * filp)

    struct tty_struct *tty;

    int index;

    idr_pre_get(&allocated_ptys, GFP_KERNEL); //和BSD的實作不同,SRV的方式在核心中查找可用的主終端

    idr_ret = idr_get_new(&allocated_ptys, NULL, &index); //得到新的項

    retval = init_dev(ptm_driver, index, &tty);//此中實作終端對的互相link

    filp->private_data = tty; //重要的指派

    devpts_pty_new(tty->link); //linux中的從終端裝置檔案是動态生成和删除的,是以linux使用了其強大的VFS機制,通過實作一個pts檔案系統來支援這種動态的增删。

SRV4使用僞終端的方式是一對多的,ptmx集合了所有的主終端,同一個檔案在不同的程序空間做區分,而BSD的方式卻直接将多個一對一的終端對開放給接口調用者。在init_dev中,最為重要的一段是:

tty = alloc_tty_struct(); //配置設定主終端

initialize_tty_struct(tty); //初始化主終端的線路規程之類,包括些許工作隊列

tty->driver = driver; //指定driver

tty->index = idx;

...

o_tty = alloc_tty_struct(); //配置設定從終端

initialize_tty_struct(o_tty); //初始化主終端的線路規程之類

o_tty->driver = driver->other; //pty_init的時候,主從driver都将other設定為對方

o_tty->index = idx;

tty_line_name(driver->other, idx, o_tty->name);

tty->link   = o_tty; //連接配接彼此,從此以後兩個終端一主一從構成一個包含很多控制功能的管道

o_tty->link = tty;

linux的sshd等程序在調用完/dev/ptmx的open之後,實際上一對終端就建立起來了,由于沒有實作SRV4的流機制,是以不需要在用ioctl将更多的協定子產品PUSH上去了,這個過程直接在init_dev中就做了,可以猜想一切基于SRV4标準實作的OS,其ptmx的open要簡單的多,是以init_dev簡單得多,不需要系統做任何事,後續的所有工作幾乎全靠調用者來用ioctl完成,比linux更靈活,但是對于很多凡人來講也更繁瑣。

     十分欣賞SRV的那種I_PUSH實作的将ip,icmp,udp等堆積成協定棧的方式--流機制,同時更樂于使用linux這種将一切都做好,但是還可以定制的OS。

 本文轉自 dog250 51CTO部落格,原文連結:http://blog.51cto.com/dog250/1271801

繼續閱讀