目录:
2 - 用户、文件操作与联机帮助1
4 - 文件系统: pwd 编写2
6 - 为用户编程:终端控制和信号8
8-进程和程序:编写命令解释器 sh17
9-可编程的 shell、shell 变量和环境:编写自己的 shell21
学习系统调用3
2 - 用户、文件操作与联机帮助
2.1 who 命令
- who 命令可以干啥?
- who 该如何使用?
- 如何自己编写一个 who?
- open 命令
- read 命令
who 命令可以干啥?
who 命令用来显示系统用户的信息
who 是如何工作的?
- 从 Unix 中学习 Unix
- 阅读联机帮助
- 搜索联机帮助
- 阅读.h 文件
- 从阅读部分得到启示
- 阅读联机帮助
-
可以用 man who 命令查看(在 WSL2 上 man 命令不好用)
注意:
-
根据 man 的信息,who 命令会查询 /var/run/utmp 的信息
- 搜索连接帮助
-
根据 man -k utmp ,可以查看关于 utmp 的信息
-
注意 utmp(5) - login records (记载了登入信息)
- 5 是小节编号,说明该帮助位于第 5 节
-
输入 man 5 utmp,查看 utmp 的帮助
可以看到其保存在 utmp.h 文件中
- 阅读.h 文件
- 通常.h 文件包含在 /usr/include 中,我们直接进入/usr/include 中查看 utmp.h 文件即可
-
发现其又包含了一次.h 文件
-
再进入查看,可以找到 utmp 结构体,其内部保存了用户数据
-
- who 的工作原理
- 打开 utmp
- 读取 utmp 结构体。 依据搜索连接帮助中 man 5 utmp ,可以得知想要直到 utmp 结构体内部信息,必须先打开/var/run/utmp 文件
- 显示记录
- 关闭 utmp
自己实现 who 命令?
#include<stdio.h> #include<stdlib.h> #include <unistd.h> #include <utmp.h> #include <fcntl.h> #include <time.h> #define SHOWHOST //包括远方的输出设备 void showtime(time_t); void show_info(struct utmp*); int main() { struct utmp current_record; int utmpfd; //从此文件描述符中读取数据 int reclen = sizeof(current_record); //读取数据的大小 // open函数用来打开指定目录的文件,UTMP_FILE代表了“/var/run/utmp” if( (utmpfd =open(UTMP_FILE,O_RDONLY)) == -1){ perror(UTMP_FILE); exit(1); } //把utmpfd读取到current_record中 while( read(utmpfd,¤t_record,reclen) == reclen) show_info(¤t_record); close(utmpfd); return 0; } void show_info(struct utmp* utbufp) { // if(utbufp->ut_type != USER_PROCESS) // return; printf("% -8.8s",utbufp->ut_user); printf(" "); printf("%s",utbufp->ut_line); printf(" "); showtime(utbufp->ut_tv.tv_sec); #ifdef SHOWHOST if(utbufp->ut_host[0] != '\0') printf("(%s)",utbufp->ut_host); #endif printf("\n"); } void showtime(time_t timeval){ char *cp; cp = ctime(&timeval); //convert time to string printf("%s",cp); }
2.2 编写 cp(读和写) ↩
4 - 文件系统: pwd 编写
学习系统调用1
文件系统介绍
文件是存放数据的地方,而目录是文件的列表。
文件系统的内部结构
文件系统是对硬盘的抽象。
一个磁盘能够存储大量数据,一个磁盘可以被划分为各区。成为分区
一个硬盘由一些磁性盘片组成。每个盘片又可以划分为很多扇区,给每个扇区分配编号,称为块编号。
-
文件系统可以用来存储文件内容、文件属性和目录。
-
文件系统存储空间结构如下:
-
存储结构
- 超级块存放文件系统本身的结构信息,比如每个区域的大小
- i -节点存储了文件属性,如文件的大小、所有者和最近修改事件
- 数据区存储了文件内容
目录入口是文件名和 i-节点号组成的对。i-节点号指向磁盘上的一个结构,该结构包含文件信息和数据块的分配,如图 4.15 所示。
文件系统的实现:创建一个文件的过程
- 当我们调用 cat spw.c > userlist 时,文件系统的内部操作是怎么回事?
创建一个文件的 4 个步骤:
- 存储属性
内核先找到一个空的 i-节点,将文件的信息记录其中。比如找到了 47。 - 存储数据
假设该文件需要三个存储磁盘,内核则从磁盘中找到 3 个自由块,比图找到了 627、200、992 - 记录分配情况
把内核分配的自由块的序列号记录到 i-节点中 - 添加文件名到目录中
新文件的名字叫 userlist,把入口(47,userlist)添加到目录文件中。
编写 pwd
- pwd 能干啥?
- pwd 的实现原理?
- 如何自己编写 pwd?
- pwd 能干啥?
pwd 可以查看从根目录到当前所在目录的路径
- pwd 的实现原理
从当前目录开始上溯,当前目录的名称为 "." 。先得到当前目录的 i - 节点号。上溯后,访问目录,查找节点号对应的名称。再上溯,直到目录树的顶端(即".."和"."的 i-节点号相同的时候)。
- 查找当前目录的 i-节点编号(通过 stat 系统调用,可以得到当前目录的状态)
- 上溯至父目录(chdir 系统调用 - 改变当前进程的所在目录)
- 在目录中,根据 i-节点编号查找目录的名称( readdir 读取目录)
- 打印
但是注意到,我们需要先打印最上层,如何保证打印的顺序?
通过递归来保证打印顺序
↩#include <stdio.h> #include <dirent.h> #include <sys/types.h> #include <sys/stat.h> #include <unistd.h> #include <string.h> #include <stdlib.h> //得到此该文件的编号 ino_t get_inode(char *); //根据编号查找文件姓名 void inum_to_name(ino_t,char*,int); //打印路径 void printpathto(ino_t ); int main(int argc) { if(argc != 1){ fprintf(stderr,"Stdin错误!"); } printpathto( get_inode(".") ); putchar('\n'); return 0; } void printpathto(ino_t this_inode) { ino_t my_node; char its_name[BUFSIZ]; if(get_inode("..") != this_inode) { //切换到上级目录 chdir(".."); //得到上级目录的名称 inum_to_name(this_inode,its_name,BUFSIZ); my_node = get_inode("."); printpathto(my_node); printf("/%s", its_name); } } //得到文件的编号 ino_t get_inode(char *fname) { struct stat info; if( stat(fname,&info) == -1) { perror(fname); exit(1); } return info.st_ino; } //根据编号得到路径,并将其存储在namebuf中 void inum_to_name( ino_t inode, char * namebuf, int buflen) { //先打开文件夹,遍历文件夹中与编号匹配的文件 DIR * dir_ptr; struct dirent* direntp; dir_ptr = opendir("."); if(dir_ptr == NULL){ perror("."); exit(1); } while ((direntp = readdir(dir_ptr)) != NULL) { if(direntp->d_ino == inode){ strncpy(namebuf,direntp->d_name,buflen); namebuf[buflen - 1] = '\0'; closedir(dir_ptr); return; } } fprintf(stderr,"error looking for inum %ld\n",inode); exit(1); }
-
学习系统调用
stat
- 作用:获取文件的状态(比如得到文件的 i-节点号、文件的大小)
- 文件状态定义如下:
struct stat { dev_t st_dev; /* file system id */ ino_t st_ino; /* file id */ mode_t st_mode; /* ownership/protection */ nlink_t st_nlink; /* number of links */ uid_t st_uid; /* user id */ gid_t st_gid; /* group id */ dev_t st_rdev; off_t st_size; /* file size in # of bytes */ unsigned long st_blksize; /* block size */ unsigned long st_blocks; /* file size in # of blocks */ unsigned long st_atime; /* time file was last accessed */ unsigned long __unused1; unsigned long st_mtime; /* time file was last modified */ unsigned long __unused2; unsigned long st_ctime; /* time file status was last changed */ unsigned long __unused3; unsigned long __unused4; unsigned long __unused5; };
具体含义: ![](https://secure2.wostatic.cn/static/maKiLfKWbgLxJpcCywx91Z/image.png?auth_key=1693225046-fNnQrm9mF1ogDDiB2tMufT-0-039ce0de0d3337a9df1129313ad1c5ec)
- 使用实例:
//得到文件的编号 ino_t get_inode(char *fname) { struct stat info; if( stat(fname,&info) == -1) { perror(fname); exit(1); } return info.st_ino; }
opendir , readdir,closedir
- 作用:打开文件并读取目录,然后关闭目录
- 实现原理
- 返回的是结构体 dirent 指目录的当前项,dirent 存放在 DIR 数据流中。 参数是 DIR 结构体指针,代表目录,是存放目录项的数据流
- 实例:读取一个目录,并且输出目录内的文件名
//**显示文件夹信息** void do_ls(char dirname[]) { //首先定义一个目录与目录项 DIR* dir_ptr; struct dirent * direntp; //读取目录名称,返回DIR目录流 if((dir_ptr = opendir(dirname)) == NULL) fprintf(stderr,"lsl: cannot open %s\n",dirname); else { //**读取目录流,得到目录项** while ((direntp = readdir(dir_ptr)) != NULL) { printf("%s\n",direntp->d_name); } //**关闭目录** closedir(dir_ptr); } }
chdir
- 作用:改变当前进程所在目录
- 使用实例:
//**切换到上级目录** chdir("..");
-
作用:在一个程序中调用另一个程序
-
函数原型:int execvp(const char __file, char const**** *****__argv)
-
注意:execvp 函数的第一个参数是程序名称,第二个参数是程序的命令行参数数组。
-
详解:
-
实例:调用 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 详解
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); }
输出:
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); }
输出:
wait/exit
-
作用:进程调用 wait 等待子进程结束
-
用法:pid = wait(&status)
-
wait 详解
-
系统调用 wait 做两件事。首先,wait 暂停调用它的进程直到子进程调用 exit(n)结束。然后,wait 取得子进程结束时传给 exit 的值,并且得到 exit 的状态。
-
statusptr 存储了返回的信号。此整数由三部分组成 --- 8 个 bit 记录退出值,7 个 bit 时记录信号序号,第八位用来指明发生错误并产生了内核映像。
-
实例: 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); }
stat
- 作用:获取文件的状态(比如得到文件的 i-节点号、文件的大小)
- 文件状态定义如下:
struct stat { dev_t st_dev; /* file system id */ ino_t st_ino; /* file id */ mode_t st_mode; /* ownership/protection */ nlink_t st_nlink; /* number of links */ uid_t st_uid; /* user id */ gid_t st_gid; /* group id */ dev_t st_rdev; off_t st_size; /* file size in # of bytes */ unsigned long st_blksize; /* block size */ unsigned long st_blocks; /* file size in # of blocks */ unsigned long st_atime; /* time file was last accessed */ unsigned long __unused1; unsigned long st_mtime; /* time file was last modified */ unsigned long __unused2; unsigned long st_ctime; /* time file status was last changed */ unsigned long __unused3; unsigned long __unused4; unsigned long __unused5; };
具体含义: ![](https://secure2.wostatic.cn/static/maKiLfKWbgLxJpcCywx91Z/image.png?auth_key=1693225046-fNnQrm9mF1ogDDiB2tMufT-0-039ce0de0d3337a9df1129313ad1c5ec)
- 使用实例:
↩ ↩//得到文件的编号 ino_t get_inode(char *fname) { struct stat info; if( stat(fname,&info) == -1) { perror(fname); exit(1); } return info.st_ino; }
opendir , readdir,closedir
- 作用:打开文件并读取目录,然后关闭目录
- 实现原理
- 返回的是结构体 dirent 指目录的当前项,dirent 存放在 DIR 数据流中。 参数是 DIR 结构体指针,代表目录,是存放目录项的数据流
- 实例:读取一个目录,并且输出目录内的文件名
↩ ↩ ↩ ↩//**显示文件夹信息** void do_ls(char dirname[]) { //首先定义一个目录与目录项 DIR* dir_ptr; struct dirent * direntp; //读取目录名称,返回DIR目录流 if((dir_ptr = opendir(dirname)) == NULL) fprintf(stderr,"lsl: cannot open %s\n",dirname); else { //**读取目录流,得到目录项** while ((direntp = readdir(dir_ptr)) != NULL) { printf("%s\n",direntp->d_name); } //**关闭目录** closedir(dir_ptr); } }
- 作用:打开文件并读取目录,然后关闭目录
chdir
- 作用:改变当前进程所在目录
- 使用实例:
//**切换到上级目录** chdir("..");
-
作用:在一个程序中调用另一个程序
-
函数原型:int execvp(const char __file, char const**** *****__argv)
-
注意:execvp 函数的第一个参数是程序名称,第二个参数是程序的命令行参数数组。
-
详解:
-
实例:调用 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 详解
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); }
输出:
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); }
输出:
wait/exit
-
作用:进程调用 wait 等待子进程结束
-
用法:pid = wait(&status)
-
wait 详解
-
系统调用 wait 做两件事。首先,wait 暂停调用它的进程直到子进程调用 exit(n)结束。然后,wait 取得子进程结束时传给 exit 的值,并且得到 exit 的状态。
-
statusptr 存储了返回的信号。此整数由三部分组成 --- 8 个 bit 记录退出值,7 个 bit 时记录信号序号,第八位用来指明发生错误并产生了内核映像。
-
实例: 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); }
6 - 为用户编程:终端控制和信号
学习系统调用1
- tcsetattr/tcgetattr
- fcntl
- signal
如何编写终端驱动?
应用场景
我们有时需要改变自己与终端的交互模式。比如输入密码的时候关闭屏幕的回显,以保证机密性。
终端驱动程序简介
驱动程序决定了用户和终端的交互模式。
我们可以选择与终端的交互模式:比如关闭回显,关闭缓冲。
如何实现编写?
思路:
- 从驱动程序获得属性(通过系统调用 tcgetattr),属性存放在 termios 结构体中
- 修改所要修改的属性
- 将修改的属性送回驱动程序(通过系统调用 tcsetattr)
举例,以下代码为一个连接开启字符回显:
编写驱动:关于位
termios 结构体存储了决定用户与终端交互状态的位:
改变位的状态即改变交互状态(比如是否开启回显,是否开启缓存)
每个属性在标志集中都占有一位。对属性的操作如下:
实例,改变回显
此例将键盘回显开或管。如果输入'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);1
//设置状态
set_mode();1
//接受输入
response = get_response^11;
//恢复出厂设置
tty_mode(0);^9;
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^12;//关闭回显 set_cr_noecho_mode()[^13]; //关闭阻塞 set_nodelay_mode();[^14]
== //得到输入,重点!!!==
response = get_response^15;//恢复出厂设置 tty_mode[^12](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()1
//得到响应 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; }
↩
保存出厂设置与恢复出厂设置(
==**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"); } } }
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()1
//得到响应 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; }
- 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; }
8-进程和程序:编写命令解释器 sh
学习系统调用1
什么是进程?
进程就是正在运行的程序。
shell 是管理进程和运行程序的进程,其主要功能有三:
- 运行程序
- 管理输入和输出
- 可编程
例子:
![image](https://b3logfile.com/file/2023/08/siyuan/1690891567944/assets/image-20230820195005-c5c70im.png)
shell 运行原理
一开始我们处在 shell 进程中,我们想要运行其他程序,就需要开辟一个新的进程。比如:shell 从用户读入字符串“ls"。shell 建立一个新的进程,然后在那个新的进程中运行 ls 程序并等待那个进程结束。
所以,为了写一个 shell,我们需要学会:
- 运行一个程序
- 建立一个进程
- 等待 exit().
编写自己的 shell
shell 最基础的功能之一就是在 shell 中可以调用其他进程。
问题 1 :如何在一个程序中运行另一个进程?
**答案:**调用了 execvp1
程序编写思路
-
建立一个字符串数组,存储用户输入(用 fgets 函数)
-
fgets 函数会保存用户输入的回车符号,因此需要专门的函数将回车符替换成
'\0'
-
调用 execvp
#include <stdio.h> #include <signal.h> #include <string.h> #include <stdlib.h> #include <unistd.h> #define MAXARGS 20 //输入指令的最大数量 #define ARGLEN 100 //一个指令最大的字符数 int execute(char *arglist[]); int main() { char *arglist[MAXARGS + 1]; //用于存储指令的数组 int numargs; char argbuf[ARGLEN]; char *makestring(); //用于将fgets最后的回车替换成'\0' numargs = 0; while(numargs < MAXARGS) { printf("Arg[%d]?",numargs); if(fgets(argbuf,ARGLEN,stdin) && *argbuf != '\n') arglist[numargs++] = makestring(argbuf); else { if(numargs > 0){ arglist[numargs] = NULL; execute(arglist); numargs = 0; } } } return 0; } ---------------------------------------------------------------- //调用execvp替换掉原来的程序 ---------------------------------------------------------------- int execute(char *arglist[]) { execvp(arglist[0],arglist); perror("execvp failed"); exit(1); } ----------------------------------------------------------------- /* 把fgets中的回车去掉 */ ----------------------------------------------------------------- char *makestring(char * buf){ char *cp; buf[strlen(buf) - 1] = '\0'; ///////////把回车替换成'\0' cp = malloc(strlen(buf) + 1); if(cp == NULL){ fprintf(stderr,"no memory\n"); exit(1); } strcpy(cp,buf); return cp; }
问题 2 :调用完其他程序后,shell 程序如何不退出?
但是上述代码有些缺陷,execvp用命令指定的程序覆盖了shell的程序代码,然后在命令指定的程序结束后退出。这样shell就不能再次接受新的命令了,为了运行新的命令,用户不得不再次运行shell。 shell如何能做到在运行程序的同时还能等待下一个命令呢?方法之一就是启动一个新的进程,由这个程序执行命令程序,再返回shell。
编程思路
shell调用fork[^19]系统调用,建立新的进程 在新的进程里调用execvp,调用用户指定的命令 父进程调用wait/exit[^20]等待子进程返回。
/* 实现一个真正的shell */ #include <stdio.h> #include <signal.h> #include <string.h> #include <stdlib.h> #include <unistd.h> #include<sys/wait.h> #include <sys/types.h> #include <signal.h> #define MAXARGS 20 #define ARGLEN 100 void execute(char *arglist[]); int main() { char *arglist[MAXARGS + 1]; int numargs; char argbuf[ARGLEN]; char *makestring(); numargs = 0; while(numargs < MAXARGS) { printf("Arg[%d]?: ",numargs); if(fgets(argbuf,ARGLEN,stdin) && *argbuf != '\n') arglist[numargs++] = makestring(argbuf); //把最后的回车去掉 else { if(numargs > 0){ arglist[numargs] = NULL; execute(arglist); numargs = 0; } } } return 0; } //将fgets函数中的回车替换成'\0' char* makestring(char * buf) { char *cp; buf[strlen(buf) - 1] = '\0'; cp = malloc(strlen(buf) + 1); if(cp == NULL){ fprintf(stderr,"no memory\n"); exit(1); } strcpy(cp,buf); return cp; } //执行用户指定的命令 void execute(char *arglist[]) { int pid,exitstatus; pid = fork(); switch (pid) { case -1: perror("fork failed"); break; //在子进程中执行程序 case 0: execvp(arglist[0],arglist); perror("execvp failed"); sleep(7); exit(1); //父进程等待子进程返回 default: signal(SIGINT,SIG_IGN); while( wait(&exitstatus) != pid) ; printf( "child exited with status %d, %d\n", exitstatus>>8,exitstatus & 0377); break; } }
↩
-
-
作用:在一个程序中调用另一个程序
-
函数原型:int execvp(const char __file, char const**** *****__argv)
-
注意:execvp 函数的第一个参数是程序名称,第二个参数是程序的命令行参数数组。
-
详解:
-
实例:调用 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 详解
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); }
输出:
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); }
输出:
↩
-
wait/exit
-
作用:进程调用 wait 等待子进程结束
-
用法:pid = wait(&status)
-
wait 详解
-
系统调用 wait 做两件事。首先,wait 暂停调用它的进程直到子进程调用 exit(n)结束。然后,wait 取得子进程结束时传给 exit 的值,并且得到 exit 的状态。
-
statusptr 存储了返回的信号。此整数由三部分组成 --- 8 个 bit 记录退出值,7 个 bit 时记录信号序号,第八位用来指明发生错误并产生了内核映像。
-
实例: 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); }
↩
-
9-可编程的 shell、shell 变量和环境:编写自己的 shell
↩
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于