什么是 shell
Linux 系统中,shell 是一个特殊的应用程序,它可以管理进程和运行程序。Linux 中有很多种 shell,每种都有自己的风格和优势。常用的 shell 都有三个主要的功能:
- 运行程序
例如,ls、cd、date 等都是一些普通程序,用 C 语言编写,并被编译成机器语言。shell 将它们载入内存并运行。也可以认为 shell 是一个程序启动器。
- 管理输入和输出
shell 不仅可以运行程序,还可以使用符号 <、> 和 | 将输入、输出重定向。告诉 shell 将进程的输入和输出连接到一个文件,或者是其他的进程。
- 可编程
shell 也是带有变量和流程控制的编程语言。
本篇文章主要讲解 shell 是如何运行一个程序的。对前面学习的进程相关的内容做一个总结。
下面我们分析一下,shell 运行程序的流程和原理。
shell 如何运行程序
shell 首先会打印提示符 $ 或 #,输入命令后,shell 就会运行这个命令,然后shell 再次打印提示符,如此反复。其背后到底发生了什么呢?
一个 shell 的主循环执行下面的 4 步:
(1)用户输入命令。
(2)shell 建立一个新进程来运行这个程序。
(3)shell 将程序从磁盘载入。
(4)程序在它的进程中运行,直到结束。
整个过程的伪代码如下:
while(! end_of_input)
{
get command /* 获取命令 */
execute command /* 执行命令 */
wait for command to finish /* 等待命令执行结束 */
}
考虑如下指令:
$ ls
cutecom.log Downloads minicom.log Public Templates work
Desktop examples.desktop Music smb test
Documents learn Pictures snap Videos
$ ps
PID TTY TIME CMD
2504 pts/0 00:00:00 bash
2799 pts/0 00:00:00 ps
$
可以用下图来展示一下事件发生的次序。其中,时间从左向右消逝。
shell 由标识为 sh 的方块代表。shell 读入用户输入的字符串 “ls”。shell 建立一个新进程,然后在这个进程中运行 ls 程序,并等待其运行结束。
要实现一个 shell,需要用到前面学过的:
- 运行一个程序
- 建立一个进程
- 等待进程退出
实现 shell 分析
1. 运行一个程序
运行一个程序可以调用 execvp()
函数来完成。详细介绍可参考
其函数原型如下:
#include <unistd.h>
int execvp(const char *file, char *const argv[]);
execvp()
载入由 file 指定的程序到当前进程,然后试图运行它。将字符串列表 argv 传递给要运行的程序。
execvp()
在环境变量 PATH 所指定的路径中查找 file 文件。
2. 创建新进程
创建一个进程可以用 fork() 函数,以及进程退出的相关内容,可参考:
系统函数 fork() 的原型为:
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
使用 fork() 创建一个新进程,在新的进程中调用 execvp()
函数,可以执行任何用户指明的程序。
如此,shell 运行程序时,不会影响其本身,并且可以运行多条命令。
进程退出,可以调用 exit()
函数,将状态信息传递给父进程。参数 status 表示进程的终止状态。
#include <stdlib.h>
void exit(int status);
3. 等待子进程退出
进程可以调用 wait()
函数来等待其子进程退出。详细内容可参考:
wait()
会暂停调用它的进程直到子进程结束,然后取得子进程结束时传递给 exit() 的值。
函数原型为:
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *wstatus);
父进程要获取子进程的退出状态,需要将一个整型变量的地址传递给 wait()
函数。
4. 综合分析
shell 实现运行一个程序的功能,其流程可以概括为:
- shell 调用 fork 创建新进程
- 用 exec 在新进程中运行用户指定的程序
- shell 通过 wait 等待新进程结束。
- 通过 wait 可以取得进程退出的状态信息。
shell 实现代码
让我们综合运用前边学习的进程知识,来实现一个简易版的 shell。
具体的代码如下,代码中含有注释:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <string.h>
#define MAXARGS 20
#define ARGLEN 100
char *makestring(char *buf);
void execute(char *argv[]);
/* 主函数 */
int main()
{
/* 字符串数组 */
char *arglist[MAXARGS + 1];
int numargs;
char argbuf[ARGLEN]; /* 接收用户输入 */
numargs = 0;
while(numargs < MAXARGS)
{
/* 打印提示符 */
printf("Arg[%d]: ", numargs);
/* 读取用户输入 */
if(fgets(argbuf, ARGLEN, stdin) && (*argbuf != '\n'))
{
/* 填充字符串数组 */
arglist[numargs++] = makestring(argbuf);
}
else
{
if(numargs > 0)
{
/* 字符串结尾添加 NULL */
arglist[numargs] = NULL;
/* 执行程序 */
execute(arglist);
numargs = 0;
}
}
}
return 0;
}
/* 执行程序,程序参数为字符串列表 */
void execute(char *argv[])
{
pid_t pid;
int exitstatus;
/* 创建新进程 */
pid = fork();
switch(pid)
{
/* 出错了 */
case -1:
{
perror("fork failed");
exit(1);
}
case 0:
{
/* 子进程,运行新程序 */
execvp(argv[0], argv);
perror("execv failed");
exit(1);
}
default:
{
/* 父进程等待子进程退出 */
while(wait(&exitstatus) != pid);
/* 打印运行结果 */
printf("child exited with status: %d, %d\n", (exitstatus >> 8), (exitstatus & 0xff));
}
}
}
/* 创建字符串 */
char *makestring(char *buf)
{
char *p = NULL;
buf[strlen(buf) - 1] = '\0';
p = malloc(strlen(buf) + 1);
if(p == NULL)
{
fprintf(stderr, "no memory\n");
exit(1);
}
strcpy(p, buf);
return p;
}
编译、运行。 并测试其运行情况:
这个 shell 程序可以接受程序名称、参数列表、运行程序、报告结果;然后再重新接收和运行其他程序。
ok,我们之前学过的内容,得到了综合运行。并且还学习了一下 shell 执行程序的原理。
总结
在此总结一下进程相关的基础内容:
- 进程是运行一个程序所需的内存空间和其他资源的集合。
- Linux 通过将可执行代码载入进程并执行它,来运行一个程序。
- 每个运行中的程序在自己的进程中。
- 进程都有唯一的进程 ID、所有者、大小等属性。
- 系统函数 fork 可以创建一个新进程。
- 一个程序可以通过 exec 函数在当前进程中执行一个新程序
- 一个程序通过调用 wait 来等待子进程结束。
- 调用进程能将一个字符串列表传递给新程序。
- 新程序能通过 exit 回传一个 8 位长的值。
- shell 通过调用 fork、exec、wait 来运行程序。
Linux 中一个重要的知识点 “进程”,先介绍到这。后面进行其他方面的学习。加油~
关注公众号【一起学嵌入式】,获取更多精彩内容