从文件的角度看设备

之前几篇文章介绍的编程是基于文件的。数据可以保存在文件中,也可以从文件中取出来做处理,再存回去。不仅如此,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. 《TCP/IP网络编程》学习笔记整理

    简介 本笔记目前已包含 <TCP/IP网络编程>中的前 5 章,后续章节会在近期内补充完整. 我在整理笔记时所考虑的是:在笔记记完后,当我需要查找某个知识点时,不需要到书中去找,只需查看笔 ...

  2. Day07_34_集合概述

    集合概述 * 主要集合概述 - 集合相当于现实世界中的容器,主要包含两种存放模式,一个一个的存(Collection), 一对一对存(Map[key,value]) - 集合中只能存储引用数据类型,不 ...

  3. webpack的构建流程

    一.运行流程 webpack 的运行流程是一个串行的过程,它的工作流程就是将各个插件串联起来 在运行过程中会广播事件,插件只需要监听它所关心的事件,就能加入到这条webpack机制中,去改变webpa ...

  4. go gin框架和springboot框架WEB接口性能对比

    1 简要概述 最近看起go lang,真的被go的goroutine(协程)惊艳到了,一句 go function(){#todo},即可完成一个并发的工作. 看到gin这个web框架时,突然就特别想 ...

  5. 11- APP性能测试GT工具的使用

    对性能测试来说有服务端的性能与客户端(APP)的性能. GT简介 1.GT(随身调)是APP的随身调测平台,它是直接运行在手机上的"集成调试环境"(IDTE) 2.利用GT,仅凭一 ...

  6. Vue学习(三)-Vue-router路由的简单使用

    一.Vue-Router环境的安装: 如果使用vue-cli脚手架搭建,项目创建过程中会提示你自否选择使用vue-router,选择使用即可, 二.路由学习 1.路由的配置    vue-cli项目自 ...

  7. C/C++ 实现VA与FOA之间的转换

    PE结构中的地址互转,这次再来系统的复习一下关于PE结构中各种地址的转换方式,最终通过编程来实现自动解析计算,最后将这个功能集成到我的迷你解析器中,本章中使用的工具是上次讲解PE结构文章中制作的CMD ...

  8. hdu4717 三分(散点的移动)

    题意:      给你一些点,这些点有各自的初始位置,移动速度和方向,问你什么时候任意两点中最长的距离最小,求时刻和此时的距离.. 思路:      感觉题目很赞,一开始想不到三分,因为么有办法证明他 ...

  9. Python小游戏 -- 猜单词

    Python初学者小游戏:猜单词 游戏逻辑:就像我们曾经英语学习机上的小游戏一样,电脑会从事先预置的词库中抽取单词,然后给出单词的字母数量,给定猜解次数,然后让玩家进行猜测,并给出每次猜测的正确字母与 ...

  10. Linux下抓包命令tcpdump的使用

    在linux下,可以使用 tcpdump 命令来抓取数据包. 主要用法如下: 过滤网卡 tcpdump -i eth0 #抓取所有经过网卡eth0数据包 tcpdump -i lo #抓取环回口的数据 ...