Linux系統程式設計【5】——stty的學習

lularible發表於2021-05-27

從檔案的角度看裝置

之前幾篇文章介紹的程式設計是基於檔案的。資料可以儲存在檔案中,也可以從檔案中取出來做處理,再存回去。不僅如此,Linux作業系統還專門為這個東西建立了一套規則,就是前期介紹的“檔案系統”。有了檔案系統,能高效的管理檔案。

那麼除了狹義上的檔案(存在磁碟中),計算機還有許多其他的資料來源,比如終端、印表機、掃描器、滑鼠、揚聲器、照相機、調變解調器等等的外部裝置。它們種類不一,管理起來是否很費勁呢?能否用檔案的思想對它們進行統一?

對於Linux來說,印表機、滑鼠、終端和磁碟檔案是同一種物件,每個裝置都被當做一個檔案,擁有檔名、i-節點號、檔案屬性等等。

裝置檔名

輸入:ls /dev,即顯示/dev資料夾中的檔案

在這裡插入圖片描述

每個載入到Linux的裝置都通過檔名錶示,這些檔案一般都存放在/dev中,但可以在任何目錄中建立裝置檔案。上圖所示的fd檔案是軟碟機,tty*是終端。

裝置的系統呼叫

裝置可以支援與所有檔案相關的系統呼叫:open、read、write、lseek、close和stat。當然,對於某些裝置,不支援其中的某些系統呼叫,如終端不支援lseek,這是由實際的需求來決定的。

從上面的描述來看,裝置就像是檔案,可以對某些裝置像檔案一樣的讀寫。比如我們採用cp命令,可以將一個檔案的內容複製到終端裝置中。或者用重定向符">"將輸出內容重定向到終端,如下圖所示:

在這裡插入圖片描述

其中tty命令是顯示當前終端檔名,再用重定向符">"將who命令的輸出重定向到當前終端,即顯示在當前終端。

裝置檔案的屬性

要看檔案屬性,常規做法就是使用ls -li命令:

在這裡插入圖片描述

上面的顯示錶明/dev/pts/1這個裝置檔案的i節點號為4,許可權位為rw--w----,一個連結,檔案所有者為lularible,所在組為tty,以及最新修改時間。"c"表示這是一個字元型裝置。

裝置檔案的i節點儲存的是指向核心子程式的指標,而不是檔案的大小和儲存列表。核心中傳輸裝置資料的子程式被稱為裝置驅動程式。在/dev/pts/1中,136和1這兩個數被稱為裝置的主裝置號和從裝置號。主裝置號確定處理該裝置實際的子程式,而從裝置號被作為引數傳遞到該子程式。

write程式的簡單實現

在知道了終端裝置可以同普通檔案一樣進行讀寫後,我們就可以著手自己實現一個write程式了。Linux中的write程式的功能是與其他終端使用者聊天,輸入man 1 write可以檢視文件描述:

在這裡插入圖片描述

執行該程式後,輸入你想要聊天的那個終端檔名,然後就可以給目標終端發訊息了。

處理邏輯就是:從main的argv中接收到目標終端檔名,然後開啟它,利用迴圈向其中寫入字元,直到退出。

原始碼如下:

/* write0.c
 * writed by lularible 2021/05/27
 */ 

#include<stdio.h>
#include<fcntl.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>

int main(int ac,char* av[])
{
	int fd;
	char name[BUFSIZ];
	char buf[BUFSIZ];
	if(ac != 2){
		fprintf(stderr,"usage:write0 ttyname\n");
		exit(1);
	}
	fd = open(av[1],O_WRONLY);
	if(fd == -1){
		perror(av[1]);
		exit(1);
	}
	printf("Please input your nickname:\n");
	scanf("%s",name);
	int name_len = strlen(name);
	name[name_len] = ':';
	name[name_len+1] = '\0';
	while(getchar() != '\n'){
		continue;
	}
	while(fgets(buf,BUFSIZ,stdin) != NULL){
		if(write(fd,name,strlen(name)) == -1 || write(fd,buf,strlen(buf)) == -1){
			break;
		}
	}
	close(fd);
}

效果如下,就這樣實現了一個簡易的終端聊天小程式。

在這裡插入圖片描述

在這裡插入圖片描述

裝置與磁碟檔案的不同

首先一個不同就是它們的i節點內容不太一樣。磁碟檔案的i節點包含指向資料塊的指標。裝置檔案的i節點包含指向核心子程式表的指標。舉例來說,對於read操作,核心首先找到檔案描述符的i節點。如果檔案是磁碟檔案,則核心通過訪問塊分配表來讀取資料。如果檔案是裝置檔案,那麼核心通過呼叫裝置驅動程式的read部分來讀取資料。

此外,程式與磁碟檔案的連線,和與裝置的連線是不同的,主要體現在連線屬性上。

磁碟連線屬性

緩衝

我們可以關閉核心的緩衝,通過三個步驟改變驅動器設定:

  1. 獲取設定
  2. 修改設定
  3. 儲存設定

具體程式碼為:

#include<fcntl.h>
int s;					//settings
s = fcntl(fd,F_GETEL)			//get flags
s |= O_SYNC;				//set SYNC bit
result = fcntl(fd,F_SETEL,s)	        //set flags
if(result == -1)
	perror("setting SYNC");

其中fcntl的函式說明如下:

在這裡插入圖片描述

fcntl對於給定的檔案描述符fd執行cmd操作。上述程式碼中,F_GETEL得到當前位集flags,變數s存放這個flags。用或操作開啟位O_SYNC,表示對write的呼叫僅能在資料寫入實際的硬體時才能返回,而不是在資料複製到核心緩衝時就執行返回操作。最後把修改好的s利用F_SETEL操作傳入核心。F_GETEL和F_SETEL讓我想到C++和Java的類中對於成員變數的獲取與修改,看來早在Linux中就已經使用這種設計思想了。

自動新增模式

自動新增模式可以讓多個程式同一時間寫入同一個檔案,即在檔案末尾新增每一個程式的要寫入的內容。

回想一下,對於單個程式,如果要在某個檔案末尾寫入內容,可以先用lseek將檔案位置指標定位到檔案末尾,然後呼叫write將內容寫入,這易如反掌,也不會產生什麼么蛾子。但如果有兩個使用者A和B都在同一時間要在同一個檔案末尾寫內容,可能會發生如下的情況:

在這裡插入圖片描述

如果A先用lseek定位到100位置,在A寫之前,B也用lseek定位到100,接著A在100處開始寫內容,B最後也從100開始寫內容,結果就是B寫的內容會覆蓋A寫的內容。lseek和write兩個操作的分離是導致上述現象的原因。

我們可以將O_APPEND置位,即開啟自動新增模式,核心會將lseek和write組合成一個原子操作,這樣就解決了上述問題。

終端連線屬性

我們敲擊鍵盤,螢幕上就瞬間出現敲擊的字元,就像是螢幕裝置與鍵盤裝置進行了“直連”。然而,我們在鍵盤上輸入字元,實際上並不是原封不動的傳遞給了程式,而是被某些中間程式作了處理。

下面一段程式碼把輸入字元列印出來:

#include<stdio.h>

int main()
{
	int c,n = 0;
	while((c = getchar()) != 'Q')
		printf("char %3d is %c code %d\n",n++,c,c);
}

執行它,鍵入hello,按Enter鍵,輸出如下:

在這裡插入圖片描述

在這裡,理應是每輸入一個字元程式就有響應,但知道我輸完"hello"按下Enter鍵之後,才有響應,輸入看起來像是被緩衝了。從輸出的第6行可以看到,ASCII碼13(Enter鍵)被10(換行符)所替代。這個例子足以說明,在裝置與程式之間,傳輸的資料被做了手腳(當然如何做手腳是人為設定的,不然就會出現不可預知的錯誤)。

終端驅動程式

在中間“做手腳”的就是所謂的終端驅動程式。用稍微專業一點的話來講,處理程式和外部裝置間資料流的核心子程式的集合就被稱為終端驅動程式或tty驅動程式。

那麼如何修改驅動程式設定?答案是:使用stty命令。到現在,終於和標題呼應上了。

輸入:man stty

在這裡插入圖片描述

stty命令可以顯示和改變終端驅動程式的設定。

顯示終端設定:

在這裡插入圖片描述

改變終端設定:

在這裡插入圖片描述

上圖中,一開始輸入who,顯示當前登入的使用者資訊。然後輸入stty -echo,表示關閉終端回顯,即輸入的字元不會在螢幕上顯示,但會將程式結果列印出來。再次輸入who,在螢幕上看不見who,卻列印了想要的內容。最後輸入stty echo開區回顯,輸入who,就又能看見了所輸內容了。

除了使用Linux提供的shell命令stty,我們還可以自己編寫程式碼來設定終端驅動。改變終端驅動程式的設定同改變磁碟檔案連線的設定一樣,也分三步:

  1. 從驅動程式獲得屬性
  2. 修改所要修改的屬性
  3. 將修改過的屬性送回驅動程式

示範程式碼如下:

#include<termios.h>
struct termios settings;
tcgetattr(fd,&settings);
settings.c_lflag |= ECHO;
tcsetattr(fd,TCSANOW,&settings);

這段程式碼所起的作用和在shell中輸入stty echo是一樣的。settings是一個termios結構體,其中包含各種終端設定的位引數,我們取其中的c_lflag位集,將回顯位置1,然後送回驅動程式。TCSANOW表示立即更新設定。

參考資料

《Understanding Unix/Linux Programming A Guide to Theory and Practice》

歡迎大家轉載本人的部落格(需註明出處),本人另外還有一個個人部落格網站:lularible的個人部落格,歡迎前去瀏覽。

相關文章