6 - 为用户编程:终端控制和信号

本贴最后更新于 572 天前,其中的信息可能已经时移世易

学习系统调用1

  • tcsetattr/tcgetattr
  • fcntl
  • signal

如何编写终端驱动?

应用场景

我们有时需要改变自己与终端的交互模式。比如输入密码的时候关闭屏幕的回显,以保证机密性。

终端驱动程序简介

驱动程序决定了用户和终端的交互模式。

我们可以选择与终端的交互模式:比如关闭回显,关闭缓冲。

image

如何实现编写?

思路:

  • 从驱动程序获得属性(通过系统调用 tcgetattr),属性存放在 termios 结构体中
  • 修改所要修改的属性
  • 将修改的属性送回驱动程序(通过系统调用 tcsetattr)

举例,以下代码为一个连接开启字符回显:

image

编写驱动:关于位

termios 结构体存储了决定用户与终端交互状态的位:

image

image

改变位的状态即改变交互状态(比如是否开启回显,是否开启缓存)

每个属性在标志集中都占有一位。对属性的操作如下:

image

实例,改变回显

此例将键盘回显开或管。如果输入'y',则终端的回显位被开启,否则被关闭。

#include <termios.h> #include <unistd.h> #include <stdlib.h> #include <stdio.h> # include<stdio.h> #include <sys/stat.h> /* 如果命令行以'y'开始,终端的回显将会开启 否则回显会被关闭 */ #define oops(s,x) {perror(s); exit(x);} int main(int argc,char* argv[]) { //指向终端的结构体 struct termios info; if(argc == 1) exit(0); int rv; rv = tcgetattr(0,&info); if(rv == -1){ perror("tcgetattr"); exit(1); } if(argv[1][0] == 'y') info.c_lflag |= ECHO; //打开回显标志位 else info.c_lflag &= ~ECHO; //关闭回显标志位 //设置终端属性 if( tcsetattr(0,TCSANOW, &info) == -1) oops("tcsetattr",2); if(info.c_lflag & ECHO) printf("echo is on,since its bit is 1\n"); else printf("echo is OFF,since its bit is 0\n"); }

如何编写一个用户程序?

应用场景

很多用户应用程序,例如,自动取款机和计算机游戏,都会向用户提出yes/no的问题。

简易版本

思路:

  • 对用户显示提示问题
  • 接受输入
  • 如果是'y',返回 0
  • 如果是'n',返回 1
#include <stdio.h> #include <stdlib.h> #include <termios.h> #define QUESTION "Do you want another transaction?" int get_response(char* ); int main() { int response; response = get_response(QUESTION); return response; } int get_response(char* ) { //输出提问 printf("%s (y/n)?",QUESTION); //等待用户输入 while (1) { switch (getchar()) { case 'y' : case 'Y' : return 0; case 'n' : case 'N' : case EOF : return 1; default : exit(1); } }

问题

简单版本中只有用户按回车键后,程序才能接受到程序。第二,用户按回车后,程序接受整行的数据并对其进行处理。

立即响应版本

版本改进

可以即时响应用户输入

思路

  • 先保存驱动原有模式
  • 设置驱动模式,关闭缓存
  • 接受输入
  • 恢复原来模式

代码实现


  • 主函数实现

#include <stdio.h>
#include <stdlib.h>
#include <termios.h>

/*
关闭缓存,程序可以立即响应输入
*/

#define QUESTION "Do you want another transaction?"
int get_response(char* );
void set_mode();
int tty_mode(int);

int main()
{
int response;
//保存状态
tty_mode(0);2
//设置状态
set_mode();3
//接受输入
response = get_response^4;
//恢复出厂设置
tty_mode^2;
return response;
}

保存出厂设置与恢复出厂设置(==**int tty_mode(int);**==​)

思路

设置一个静态变量用来保存原有的驱动状态

//读取状态,保存状态 int tty_mode(int how) { static struct termios origin_mode; if(how == 0) tcgetattr(0,&origin_mode); else return tcsetattr(0,TCSANOW,&origin_mode); }

设置驱动模式,关闭缓存(**==set_mode();==**​​​​​)

思路

  • 关闭缓存标志位
void set_mode() { struct termios ttystate; tcgetattr(0,&ttystate); //读取当前的终端状态 ttystate.c_lflag &= ~ICANON; //关闭缓冲 ttystate.c_cc[VMIN] = 1; //每次接收一个字符 tcsetattr(0,TCSANOW,&ttystate); // 加载状态 }

接受输入(**==get_response(QUESTION);==**​​​)
int get_response(char* ) { int input; //输出提问 printf("%s (y/n)?",QUESTION); //等待用户输入 while (1) { switch (input = getchar()) { case 'y' : case 'Y' : return 0; case 'n' : case 'N' : case EOF : return 1; default : printf("\nCannot understand %c ",input); printf("Please type y or no \n"); } } }

问题

如果这个程序运行在 ATM 上,而顾客在输入 y 或 n 之前走开了,将会怎样?下一个顾客跑来按 y,就能进入那个离开的顾客账号。所以用户程序包含超时特征,会变得更安全。


超级版本:等待输入版本

版本改进

此版本具有超时特征。通过设置终端驱动程序,使之不等待输人来实现这个特征,先检查看是否有输入,如果发现没有输人,则先睡眠几秒钟,然后继续检查输人。如此尝试 3 次之后放弃。

思路

注意到,我们的程序可以检测用户不输入状态。如何做到?

默认情况下终端是有阻塞模式的,程序会等待用户输入,然后才检测用户输入。

如果我们处于阻塞模式,程序就会检测不到我们没有输入,所以我们必须把阻塞关掉。

如果我们关闭阻塞模式,程序会直接判断我们的输入,如果我们没输入,则 read 程序会返回 0。

主要思路(输入思路)

  • 关闭回显、缓冲和阻塞 - 解决判断用户不输入的问题

  • 睡眠!

  • 等待输入 1 - 无视错误输入

    • 如果检测不到 yYnN,就会一直在 while 循环里,做不到其他事情

    ==**while (strchr("yYnN",c) == NULL) ; //不论如何,程序是不会锁死在while循环里的**==

    问题:但是因为关闭了阻塞,所以如果我们不输入或者错误输入,strchr 会让 while 卡死在循环里

  • 等待输入 2 - 解决无法退出循环的问题

    **==while ((c = getchar()) != EOF && strchr("yYnN",c) == NULL) ;==**​​

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

/*
万一有人在输入 y 之前走开了,程序会不安全。
因此要加入超时判断
*/

/*
设计思路:
设置终端驱动程序,使之不等待输入来实现这个特征。先检查是否由输入,然后沉睡,再检查输入
如此往复 3 次后退出
*/
#define ASK "Do you want another transaction?"
#define TRIES 3 //尝试 3 次后退出
#define SLEEPTIME 3 //沉睡事件间隔
#define BEEP putchar('\a'); //警告用户

int get_response(char* ,int);
void set_cr_noecho_mode();
void tty_mode(int);
void set_nodelay_mode();
int get_ok_char();

int main()
{
int response;
//保存状态
tty_mode^5;

//关闭回显 set_cr_noecho_mode()[^6]; //关闭阻塞 set_nodelay_mode();[^7]

==​ //得到输入,重点!!!==
response = get_response^8;

//恢复出厂设置 tty_mode[^5](1); return response;

}

tty_mode(0/1);
//读取状态,保存状态 void tty_mode(int how) { static struct termios origin_mode; static int origin_flags; if(how == 0){ tcgetattr(0,&origin_mode); origin_flags = fcntl(0,F_GETFL); } else{ tcsetattr(0,TCSANOW,&origin_mode); fcntl(0,F_SETFL,origin_flags); } }

set_cr_noecho_mode()
//关闭回显与缓冲 void set_cr_noecho_mode() { static struct termios ttystate; tcgetattr(0,&ttystate); //读取当前的终端状态 ttystate.c_lflag &= ~ICANON; //关闭缓冲 ttystate.c_lflag &= ~ECHO; //关闭回显 ttystate.c_cc[VMIN] = 1; //每输入一个字符响应一次 tcsetattr(0,TCSANOW,&ttystate); //加载设置 }

set_nodelay_mode();
//将文件描述符设置为非阻塞模式 void set_nodelay_mode() { int termflags; termflags = fcntl(0,F_GETFL); //获取文件当前的状态位 termflags |= O_NDELAY; //改变状态为非阻塞模式 fcntl(0,F_SETFL,termflags); //加载状态位 }

get_response ()
  • 主要函数:get_ok_char()9
//得到响应 int get_response(char* question , int maxtries ) { int input; //输出提问 printf("%s (y/n)?",question); fflush(stdout); //清空缓冲区,强制输出 //等待用户输入 while (1) { printf("sleep now \n"); sleep(SLEEPTIME); printf("sleep finish \n"); input = tolower(get_ok_char()); //得到下一个字符,且将大写字母转为小写字母 if(input == 'y') return 0; if(input == 'n') return 1; if(maxtries-- == 0){ //超时 printf("超时退出\n"); return 2; } BEEP; } }

  • get_ok_char()

    //得到下一个字符 int get_ok_char() { int c; //查找字符c,由于关闭了阻塞模式,故不会卡在while循环 //程序会直接读取用户输入,在不输入的情况下,getchar与strchr得到的是EOF与NULL while ((c = getchar()) != EOF && strchr("yYnN",c) == NULL) ; return c; }


  1. 学习系统调用

    • 作用:在一个程序中调用另一个程序

    • 函数原型:int execvp(const char __file, ​char​ ​const****​​ ​​*****__argv)

    • 注意:execvp 函数的第一个参数是程序名称,第二个参数是程序的命令行参数数组。

    • 详解:

      image

    • 实例:调用 ls

      #include <stdio.h> #include <unistd.h> #include <stdlib.h> int main() { char *arglist[3]; arglist[0] = "ls"; arglist[1] = "-l"; arglist[2] = 0; printf("*** About to exec ls -l\n"); execvp("ls",arglist); printf("*** ls is done. bye\n"); }

    fork

    • fork 的意义

      我们在用 execvp 调用新的进程后,会把原来的进程给替换掉。比如说在我们自己编写的 shell 进程中调用 ls 后,shell 进程也被关闭了。

      而我们希望 execvp 调用后,还能返回到原来的 shell 进程中。

      所以我们可以通过 fork 建立一个新的进程,然后在新进程中调用 execvp,父进程调用 wait 等待新进程执行完毕,当新进程执行 exit 时,父进程就会收到信号,然后父进程继续运行

    • fork 详解

      image

    fork 的简单应用

    /* 建立新的进程 */ #include <stdio.h> #include <sys/types.h> #include <unistd.h> int main() { int ret_from_fork,mypid; mypid = getpid(); printf("Before: my pid is %d\n",mypid); ret_from_fork = fork(); sleep(1); printf("After: my pid is %d, fork() said %d\n", getpid(), ret_from_fork); }

    输出:

    image

    fork 的特征(区分父进程和子进程)

    • fork 会建立一个新的进程
    • fork()函数会返回进程号,在子进程中,返回的进程号是 0;父进程中,返回的进程号是子进程的进程号

    • 实例:
    /* 判断自己是子进程还是父进程 */ #include <stdio.h> #include <sys/types.h> #include <unistd.h> int main() { int fork_rv; printf("Before : my pid is %d\n",getpid()); fork_rv = fork(); //在子进程中,fork()返回0 if(fork_rv == -1) perror("fork"); else if(fork_rv == 0) printf("I am the child. my pid = %d\n",getpid()); else printf("I am the parent. my child is %d\n",fork_rv); }

    输出:

    image

    wait/exit

    • 作用:进程调用 wait 等待子进程结束

    • 用法:pid = wait(&status)

    • wait 详解

      image

    • 系统调用 wait 做两件事。首先,wait 暂停调用它的进程直到子进程调用 exit(n)结束。然后,wait 取得子进程结束时传给 exit 的值,并且得到 exit 的状态。

    • statusptr 存储了返回的信号。此整数由三部分组成 --- 8 个 bit 记录退出值,7 个 bit 时记录信号序号,第八位用来指明发生错误并产生了内核映像。

    image

    • 实例: waitdemo.c

      该例子显示了子进程调用 exit 是如何触发 wait 返回父进程并如何得到子进程返回时的状态 的。

    #include <stdio.h> #include <sys/types.h> #include <unistd.h> #include <stdlib.h> #include<sys/wait.h> #define DELAY 5 int main() { int newpid; void child_code(), parent_code(); printf("before: mypid is %d\n",getpid()); if( (newpid = fork()) == -1) perror("fork"); else if( newpid == 0) child_code(DELAY); //执行子进程的代码 else parent_code(newpid); //父进程代码 } //子进程执行的代码 void child_code(int delay) { printf("child %d here . will sleep for %d seconds\n",getpid(),delay); sleep(delay); printf("child done about to exit\n"); exit(17); } //父进程代码,等待子进程结束 void parent_code(int childpid) { int wait_rv; //return value from wait() int child_status; int high_8 , low_7, bit_7; //等待子进程结束 wait_rv = wait(&child_status); //子进程结束后,执行以下代码 printf("child_status: %p\n",*(&child_status) >> 8); printf("done waiting fot %d.&Wait returned: %d\n",childpid,wait_rv); //得到子进程退出时的状态 high_8 = child_status >> 8; //1111 1111 0000 0000 low_7 = child_status &0x7F; //0000 0000 0111 1111 bit_7 = child_status & 0x80; //0000 0000 1000 0000 printf("status: exit = %d, sig = %d, core = %d\n",high_8, low_7 ,bit_7); }

    image

  2. 保存出厂设置与恢复出厂设置(==**int tty_mode(int);**==​)
  3. 设置驱动模式,关闭缓存(**==set_mode();==**​​​​​)
  4. 接受输入(**==get_response(QUESTION);==**​​​)
  5. tty_mode(0/1);
  6. set_cr_noecho_mode()
  7. set_nodelay_mode();
  8. get_response ()
    • get_ok_char()
    //得到下一个字符 int get_ok_char() { int c; //查找字符c,由于关闭了阻塞模式,故不会卡在while循环 //程序会直接读取用户输入,在不输入的情况下,getchar与strchr得到的是EOF与NULL while ((c = getchar()) != EOF && strchr("yYnN",c) == NULL) ; return c; }

  • C

    C 语言是一门通用计算机编程语言,应用广泛。C 语言的设计目标是提供一种能以简易的方式编译、处理低级存储器、产生少量的机器码以及不需要任何运行环境支持便能运行的编程语言。

    85 引用 • 165 回帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...