从文件的角度看设备
之前几篇文章介绍的编程是基于文件的。数据可以保存在文件中,也可以从文件中取出来做处理,再存回去。不仅如此,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部分来读取数据。
此外,进程与磁盘文件的连接,和与设备的连接是不同的,主要体现在连接属性上。
磁盘连接属性
缓冲
我们可以关闭内核的缓冲,通过三个步骤改变驱动器设置:
- 获取设置
- 修改设置
- 存储设置
具体代码为:
#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,我们还可以自己编写代码来设置终端驱动。改变终端驱动程序的设置同改变磁盘文件连接的设置一样,也分三步:
- 从驱动程序获得属性
- 修改所要修改的属性
- 将修改过的属性送回驱动程序
示范代码如下:
#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的个人博客,欢迎前去浏览。