Linux系統編程【5】——stty的學習


從文件的角度看設備

之前幾篇文章介紹的編程是基於文件的。數據可以保存在文件中,也可以從文件中取出來做處理,再存回去。不僅如此,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的個人博客,歡迎前去瀏覽。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM