从文件的角度看设备

之前几篇文章介绍的编程是基于文件的。数据可以保存在文件中,也可以从文件中取出来做处理,再存回去。不仅如此,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的个人博客,欢迎前去浏览。

Linux系统编程【5】——stty的学习的更多相关文章

  1. Linux 系统编程 学习 总结

    背景 整理了Liunx 关于 进程间通信的 很常见的知识. 目录 与 说明 Linux 系统编程 学习:000-有关概念 介绍了有关的基础概念,为以后的学习打下基础. Linux 系统编程 学习:00 ...

  2. Linux 系统编程 学习:00-有关概念

    Linux 系统编程 学习:00-有关概念 背景 系统编程其实就是利用系统中被支持的调度API进行开发的一个过程. 从这一讲开始,我们来介绍有关Linux 系统编程的学习. 知识 在进行Linux系统 ...

  3. Linux 系统编程 学习:01-进程的有关概念 与 创建、回收

    Linux 系统编程 学习:01-进程的有关概念 与 创建.回收 背景 上一讲介绍了有关系统编程的概念.这一讲,我们针对 进程 开展学习. 概念 进程的身份证(PID) 每一个进程都有一个唯一的身份证 ...

  4. Linux 系统编程 学习:02-进程间通信1:Unix IPC(1)管道

    Linux 系统编程 学习:02-进程间通信1:Unix IPC(1)管道 背景 上一讲我们介绍了创建子进程的方式.我们都知道,创建子进程是为了与父进程协作(或者是为了执行新的程序,参考 Linux ...

  5. Linux 系统编程 学习:03-进程间通信1:Unix IPC(2)信号

    Linux 系统编程 学习:03-进程间通信1:Unix IPC(2)信号 背景 上一讲我们介绍了Unix IPC中的2种管道. 回顾一下上一讲的介绍,IPC的方式通常有: Unix IPC包括:管道 ...

  6. Linux 系统编程 学习:04-进程间通信2:System V IPC(1)

    Linux 系统编程 学习:04-进程间通信2:System V IPC(1) 背景 上一讲 进程间通信:Unix IPC-信号中,我们介绍了Unix IPC中有关信号的概念,以及如何使用. IPC的 ...

  7. Linux 系统编程 学习:05-进程间通信2:System V IPC(2)

    Linux 系统编程 学习:05-进程间通信2:System V IPC(2) 背景 上一讲 进程间通信:System V IPC(1)中,我们介绍了System IPC中有关消息队列.共享内存的概念 ...

  8. Linux 系统编程 学习:06-基于socket的网络编程1:有关概念

    Linux 系统编程 学习:006-基于socket的网络编程1:有关概念 背景 上一讲 进程间通信:System V IPC(2)中,我们介绍了System IPC中关于信号量的概念,以及如何使用. ...

  9. Linux 系统编程 学习:07-基于socket的网络编程2:基于 UDP 的通信

    Linux 系统编程 学习:07-基于socket的网络编程2:基于 UDP 的通信 背景 上一讲我们介绍了网络编程的一些概念.socket的网络编程的有关概念 这一讲我们来看UDP 通信. 知识 U ...

  10. Linux 系统编程 学习:09-线程:线程的创建、回收与取消

    Linux 系统编程 学习:09-线程:线程的创建.回收与取消 背景 我们在此之前完成了 有关进程的学习.从这一讲开始我们学习线程. 完全的开发可以参考:<多线程编程指南> 在Linux ...

随机推荐

  1. DevEco Device Tool 2.1 Beta1 的Hi3861在Windows平台的编译体验

    DevEco Device Tool迎来了2.1 Beta1,其中的亮点之一是:支持Hi3861开发板的源码在Windows平台编译.带着浓厚的兴趣,第一时间做了一次体验. 首先在官网下载" ...

  2. Vue CLI 是如何实现的 -- 终端命令行工具篇

    Vue CLI 是一个基于 Vue.js 进行快速开发的完整系统,提供了终端命令行工具.零配置脚手架.插件体系.图形化管理界面等.本文暂且只分析项目初始化部分,也就是终端命令行工具的实现. 0. 用法 ...

  3. 自动化kolla-ansible部署ubuntu20.04+openstack-victoria之镜像制作centos6.5-14

    自动化kolla-ansible部署ubuntu20.04+openstack-victoria之镜像制作centos6.5-14 欢迎加QQ群:1026880196 进行交流学习 制作OpenSta ...

  4. Redhat 安装gitlab

    以下为8月23日左右记录的,没有发布.今日整理大概记录下. 安装依赖包 yum install -y curl policycoreutils-python openssh-server openss ...

  5. 前端进阶(2)使用fetch/axios时, 如何取消http请求

    前端进阶(2)使用fetch/axios时, 如何取消http请求 1. 需求 现在前端都是SPA,我们什么时候需要取消HTTP请求呢? 当我们从一个页面跳转到另外一个页面时,如果前一个页面的请求还没 ...

  6. ASP.Net 管道模型 VS Asp.Net Core 管道 总结

    1 管道模型  1 Asp.Net Web Form管道 请求进入Asp.Net工作进程后,由进程创建HttpWorkRequest对象,封装此次请求有关的所有信息,然后进入HttpRuntime类进 ...

  7. 【故障公告】数据库服务器 CPU 100% 引发网站故障

    悄悄地它又突然来了 -- 数据库服务器 CPU 100% 问题,上次光临时间是 3-30 8:48,这次是 4-28 9:41. 这次我们做出了快速反应,发现后立即进行主备切换,这次一次切换成功,CP ...

  8. Selenium3自动化测试【16】元素定位之Tag

    @ 目录 1. 通过tag name定位Bing案例 2. 通过tag name定位一组元素案例 3.[测试全系列视频课程]请点击我哦..... tag name方法是通过对HTML页面中tag na ...

  9. SQL优化-大数据量分页优化

    百万数据量SQL,在进行分页查询时会出现性能问题,例如我们使用PageHelper时,由于分页查询时,PageHelper会拦截查询的语句会进行两个步骤 1.添加 select count(*)fro ...

  10. 路由器逆向分析------sasquatch和squashfs-tools工具的安装和使用

    本文博客地址:http://blog.csdn.net/qq1084283172/article/details/68942660 一.sasquatch工具的安装和使用 sasquatch工具支持对 ...