这个是暂时到后台执行
要调回来 输入 fg
直接ctrl+z
这个是暂时到后台执行
要调回来 输入 fg转载于:https://www.cnblogs.com/jiufen/p/5013710.html
- 把任务放到后台用 & 和 Ctrl+z
- 让后台任务从停止状态转为运行状态用 bg %N
- 把后台任务调回到前台用 fg %N
- 查看所有任务用jobs
了解什么是 Linux ,并介绍命令行和 shell 在 Linux 整体架构中的位置
1.1 什么是 Linux
- 下图中的每一部分在 Linux 中各司其职,协同构成一个完整的 Linux 系统
完整的 Linux 系统
1.1.1 深入探究 Linux 内核
- 内核是 Linux 的核心
- 内核控制着计算机系统上所有的硬件和软件,在必要时分配硬件,并根据需要执行软件
- 第一个版本的 Linux 内核是由 Linus Torvalds 读大学时编写的
- 内核主要负责以下四种功能
内核主要功能
1.1.1.1 系统内存管理
- 内核的主要功能之一就是内存管理
- 内核不仅管理服务器上的可用物理内存,还可以创建和管理虚拟内存
- 内核通过硬盘上的存储空间来实现虚拟内存,这个空间称为 交换空间( Swap Space )
- 内核不断地在交换空间和实际物理内存之间 反复交换虚拟内存中的内容 ,让系统以为自己拥有比物理内存更多的可用内存
- 交换示意入下图,可以看出是由内核来统筹三者之间的数据交换 内存存储单元按组划分为很多块,这些块称为 页面( Page ) 内核将每个内存页面放在物理内存或交换空间 内核同时会维护一个 内存页面表 ,在这个表中指定哪些页面在物理内存中,哪些被换到虚拟内存中 内核会记录哪些内存页面正在被使用,并自动把一段时间没有被使用的内存页面复制到交换空间中,这个过程被称为 换出( Swapping Out )
内核工作原理
- 只要 Linux 在运行,为运行中的程序换出内存页面的过程就不会停止,如下图 当程序要访问一个已经被换出的内存页面时 内核必须从物理内存中换出另外一个内存页面用于让出空间 然后再从交换空间换入被访问的内存页面
内核工作流程
1.1.1.2 软件程序管理
- Linux 将运行中的软件程序称为 进程
- 进程可以在前台运行,将输入显示在屏幕上,也可以在后台运行,隐藏到幕后
- Linux 中所有的进程都是由内核控制和管理
- 内核创建第一个进程 init 进程,用于启动系统上所有其他进程
- 内核在启动任何其他进程时,都会在虚拟内存中给新进程分配一块专有空间来存储进程用到的数据和代码 开机时需要启动或停止的应用脚本一般放置在 /etc/init.d 中
- init 进程 存在 运行级( Run Level ) 的概念
- Linux 一共有 5 个启动运行级: 只启动基本的系统进程以及一个控制台终端进程,这被称为 单用户模式 通常用于在系统出现问题时进行紧急的文件系统维护,相当于 Windows 的安全模式 还没提到 标准的运行级,可以运行大多数应用程序,例如网络支持程序 还没提到 常见的运行级,可以运行图形化界面,允许通过图形化界面登录系统
- Linux 可以通过调整启动运行级来控制整个系统的功能
1.1.1.3 硬件设备管理
- 内核可以管理硬件设备
- Linux 想要和任何设备通信之前,都需要在内核代码中加入该设备的 驱动程序代码 驱动程序代码相当于应用程序和硬件设备的中间人,允许内核与设备之间交换数据
- 内核中有以下两种方法用于插入设备驱动程序代码 编译进内核的设备驱动代码 每次都需要重新编译内核,非常低效 可插入内核的设备驱动模块 可以方便的实现热插拔,无需重新编译内核
- 硬件设备被 Linux 识别为特殊的文件,称为 设备文件 ,有以下三种类型 字符型设备文件 处理数据时每次只能处理一个字符,例如调制解调器 块设备文件 处理数据时每次能处理大块数据,例如硬盘 网络设备文件 进行数据包发送和接收数据,例如网卡
- Linux 会为每个设备文件创建一个 节点 ,用于对这些设备文件进行唯一标识 每个节点都是一个键值对,键是主设备号,相当于类别,值是副设备号,指具体设备
1.1.1.4 文件系统管理
- 内核支持通过不同类型的文件系统从硬盘读写数据
- 内核采用 虚拟文件系统( Virtual File System ,VFS ) 作为与每个文件系统交互的接口 当每个文件系统被挂载和使用时,VFS 都会将信息缓存到内存中
- 下图列出了 Linux 支持用于读写数据的标准文件系统
Linux 支持用于读写数据的标准文件系统
1.1.2 GNU 工具
- GNU( GNU’s Not Unix )组织在开源软件( Open Source Software ,OSS )的软件理念下开发了一套完整的 Unix 工具
- 将 Linux 内核和 GNU 操作系统工具整合后,就产生了一款完整的、功能丰富的免费操作系统
1.1.2.1 核心 GNU 工具
- 该工具的主要作用是为 Linux 系统管理员设计出一套类似于 Unix 的环境
- 供 Linux 使用的核心工具被称为 GNU Core Utilities 软件包,包含以下三个部分 处理文件 操作文本 管理进程
1.1.2.2 shell
- GNU / Linux shell 是一种特殊的交互式工具,为用户提供以下功能 启动程序 管理文件系统中的文件 运行在 Linux 上的进程
- shell 的核心是命令行提示符
- 将多个 shell 命令放入文件中作为程序执行,这被称为 shell 脚本
- 所有 Linux 发行版默认的 shell 都是由 GNU 组织开发的 Bash shell Bash 名称由来是根据 Unix 原生的 Bournce shell 修改而来,名为 Bournce again shell
- 下图列出了 Linux 支持的 shell 类型
Linux 支持的 shell 类型
1.1.3 Linux 桌面环境
- Linux 之所以提供桌面环境,很大因素是因为受到了 Windows 流行的影响
1.1.3.1 X Windows 系统
- X Windows 是图形显示的核心部分
- X Windows 是直接和 PC 上的显卡及显示器打交道的底层程序
- X Windows 控制着 Linux 程序如何在电脑上显示出窗口和图形
- X Windows 是底层软件包,其实现形式有以下几种: X.org 提供了 X Windows 的开源实现,支持当前市面上很多新显卡 Wayland 被 Fedora Linux 发行版采用 Mir 显示服务器由 Ubuntu Linux 发行版研发
1.1.3.2 KDE 桌面
- KDE( K Desktop Environment )在 1996 年作为开源项目被发布
- KDE 会生成一个类似于 Windows 的图形化桌面环境
1.1.3.3 GNOME 桌面
- GNOME( the GNU Network Object Model Environment )GNU 网络对象模型环境,在 1999 年发布
- 现在已经是大部分 Linux 发行版默认的桌面环境
- Red Hat Linux 大量采用该桌面环境
1.1.3.4 Unity 桌面
- 由 Ubuntu 自行研发的桌面环境
- 目标是 为工作站、平板电脑以及移动设备提供一致的桌面体验 就目前来说,平台电脑和移动设备这块已经基本失败了,主要还是缺乏生态
1.1.3.5 其他桌面
- 图形化桌面环境的弊端在于,其 需要占用想当一部分的系统资源 来保证运行正常,这和 Linux 诞生的初衷是相违背的
- 下图是 Linux 上存在的一些其他图形化桌面
Linux 上存在的一些其他图形化桌面
1.2 Linux 发行版
- 发行版 —— 完成的 Linux 系统包
1.2.1 核心 Linux 发行版
- 包含内核、一个或多个图形化桌面环境以及预编译好的几乎所有能见到的 Linux 应用
- 目标是提供一站式的完整 Linux 安装
- 下图是 Linux 比较流行的核心发行版
Linux 比较流行的核心发行版
1.2.2 特定用途的 Linux 发行版
- 由于核心版体积庞大、配置繁琐,导致安装过程异常复杂,所以出现了针对特性场景使用的 Linux 发行版
- 下图是 Linux 比较流行的特定用途发行版
Linux 比较流行的特定用途发行版
1.2.3 Linux LiveCD 样本系统
- 可以在光驱中通过引导直接运行的 Linux 版本,不需要安装就可以看到 Linux 的具体内容
- 大部分特定用户的 Linux 发行版都会推出 Linux LiveCD 版本
- 但由于是从光驱读取的原因,该版本存在以下两大缺陷: 受制于光驱的读取速度,应用程序运行起来比较慢 无法从光驱向 CD 写入数据,所以对系统做的任何修改再下一次加载时都会失效
- 下图是 Linux 比较流行的 LiveCD 样本系统
Linux 比较流行的 LiveCD 样本系统
1.3 小结
- Linux 内核是系统的核心,控制着内存、程序和硬件之间的交互
- GNU 工具也是 Linux 系统中的一个重要部分
- 现在的 Linux 可以支持多种图形化桌面环境
- Linux 发行版就是把 Linux 的不同部分汇集起来组成一个易于安装的包
后台私信【架构】获取c/c++ Linux后台服务器开发相关学习视频
转自:http://www.cppblog.com/cuijixin/archive/2008/03/14/44463.html
当我们在Linux下的命令行输入一个命令之后,这背后发生了什么?
1、什么是命令行接口
用户使用计算机有两种常见的方式,一种是图形化的接口(GUI),另外一种则是命令行接口(CLI)。对于图形化的接口,用户点击某个图标就可启动后台的某个程序;对于命令行的接口,用户键入某个程序的名字就可启动某个程序。这两者的基本过程是类似的,都需要查找程序文件在磁盘上的位置,加载到内存并通过不同的解释器进行解析和运行。下面以命令行为例来介绍程序执行那一刹那发生的一些事情。
首先来介绍什么是命令行?命令行就是commandline,很直观的概念就是系统启动后的那个黑屏幕:有一个提示符,并有光标在闪烁的那样一个终端,一般情况下可以用CTRL+ALT+F1-6切换到不同的终端;在GUI界面下也会有一些伪终端,看上去和系统启动时的那个终端没有什么区别,也会有一个提示符,并有一个光标在闪烁。就提示符和响应用户的键盘输入而言,它们两者在功能上是一样的,实际上它们就是同一个东西,你用下面的命令就可以把它们打印出来。
Quote: $ echo$SHELL #打印当前SHELL,当前运行的命令行接口程序
/bin/bash
$ echo $$ #该程序对应的进程ID,$$是一个比较特殊的环境变量,它存放了当前进程ID
1481
$ ps -C bash #通过PS命令查看
PIDTTY TIME CMD
1481pts/0 00:00:00 bash
从上面的操作结果可以看出,当前命令行接口实际上是一个程序,那就是/bin/bash,它是一个实实在在的程序,它打印提示符,接受用户输入的命令,分析命令序列并执行然后返回结果。不过/bin/bash仅仅是当前使用的命令行程序之一,还有很多具有类似功能的程序,比如/bin/tcsh,bin/ash等。不过这里主要来讨论bash了,讨论它自己是怎么启动的,它怎么样处理用户的输入命令等后台细节?
1.2 /bin/bash是什么时候启动的
1.2.1 /bin/bash
先通过CTRL+ALT+F1切换到一个普通终端下面,一般情况下看到的是XXX login:提示输入用户名,接着是提示输入密码,然后呢?就直接登录到了我们的命令行接口。实际上正是你输入正确的密码后,那个程序把/bin/bash给启动了。那是什么东西提示"XXXlogin:"的呢?正是/bin/login程序,那/bin/login程序怎么知道要启动/bin/bash,而不是其他的/bin/tcsh呢?
/bin/login程序实际上会检查我们的/etc/passwd文件,在这个文件里头包含了用户名、密码和该用户的登录shell。密码和用户名匹配用户的登录,而登录shell则作为用户登录后的命令行程序。看看/etc/passwd中典型的这么一行:
Quote: $ cat /etc/passwd | grep falcon
falcon:x:1000:1000:falcon,,,:/home/falcon:/bin/bash
这个是我用的帐号的相关信息哦,看到最后一行没?/bin/bash,这正是我登录用的命令行解释程序。至于密码呢,看到那个x没?这个x说明我的密码被保存在另外一个文件里头/etc/shadow,而且密码是经过加密的。至于这两个文件的更多细节,看manual吧。
我们怎么知道刚好是/bin/login打印了"XXXlogin"呢?现在回顾一下很早以前学习的那个strace命令。我们可以用strace命令来跟踪/bin/login程序的执行。
跟上面一样,切换到一个普通终端,并切换到root用户,用下面的命令:
Quote: $ strace -f -o strace.out/bin/login
退出以后就可以打开strace.out文件,看看到底执行了哪些文件,读取了哪些文件。从中我们可以看到正是/bin/login程序用execve调用了/bin/bash命令。通过后面的演示,我们发现/bin/login只是在子进程里头用execve调用了/bin/bash,因为在启动/bin/bash后,我们发现/bin/login并没有退出。
1.2.2 /bin/login
那/bin/login又是怎么起来的呢?
下面再来看一个演示。先在一个可以登陆的终端下执行下面的命令。
Quote: $ getty 38400 tty8 linux
getty命令停留在那里,貌似等待用户的什么操作,现在切回到第8个终端,是不是看到有"XXXlogin:"的提示了。输入用户名并登录,之后退出,回到第一个终端,发现getty命令已经退出。
类似地,我们也可以用strace命令来跟踪getty的执行过程。在第一个终端下切换到root用户。执行如下命令,
Quote: $ strace -f -o strace.out getty 38400 tty8linux
同样在strace.out命令中可以找到该命令的相关启动细节。比如,我们可以看到正是getty程序用execve系统调用执行了/bin/login程序。这个地方,getty是在自己的主进程里头直接执行了/bin/login,这样/bin/login将把getty的进程空间替换掉。
1.2.3 /sbin/getty
这里涉及到一个非常重要的东西了:/sbin/init,通过maninit你可以查看到该命令的作用,它可是“万物之王”(init is the parent of all processeson thesystem)哦。它是Linux系统默认启动的第一个程序,负责进行Linux系统的一些初始化工作,而这些初始化工作的配置则是通过/etc/inittab来做的。那么来看看/etc/inittab的一个简单的example吧,可以通过maninittab查看相关帮助。
整个配置文件的语法非常简单,就是下面一行的重复,
Quote: id:runlevels:action:process
- id就是一个唯一的编号,不用管它,一个名字而言,无关紧要。
- runlevels是运行级别,整个还是比较重要的,理解运行级别的概念很有必要,它可以有如下的取值,
Quote: 0 is halt.
1 is single-user.
2-5 are multi-user.
6 is reboot.
不过,真正在配置文件里头用的是1-5了,而0和6非常特别,除了用它作为init命令的参数关机和重启外,似乎没有哪个“傻瓜”把它写在系统的配置文件里头,让系统启动以后就关机或者重启。1代表单用户,而2-5则代表多用户。对于2-5可能有不同的解释,比如在slackware12.0上,2,3,5被用来作为多用户模式,但是默认不启动X windows(GUI接口),而4则作为启动Xwindows的运行级别。
- action是动作,它也有很多选择,我们关心几个常用的
initdefault用来指定系统启动后进入的运行级别,通常在/etc/inittab的第一条配置,如
Quote: id:3:initdefault:
这个说明默认运行级别是3,即多用户模式,但是不启动X window的那种。
sysinit指定那些在系统启动时将被执行的程序,例如
Quote: si:S:sysinit:/etc/rc.d/rc.S
[quote]
在maninittab中提到,对于sysinit,boot等动作,runlevels选项是不用管的,所以我们可以很容易解读这条配置:它的意思是系统启动时将默认执行/etc/rc.d/rc.S文件,在这个文件里你可直接或者间接的执行你想让系统启动时执行的任何程序,完成系统的初始化。
wait,当进入某个特别的运行级别时,指定的程序将被执行一次,init将等到它执行完成,例如
[quote]
rc:2345:wait:/etc/rc.d/rc.M
这个说明无论是进入运行级别2,3,4,5中哪一个,/etc/rc.d/rc.M将被执行一次,并且有init等待它执行完成。
ctrlaltdel,当init程序接收到SIGINT信号时,某个指定的程序将被执行,我们通常通过按下CTRL+ALT+DEL,这个默认情况下将给init发送一个SIGINT信号。如果我们想在按下这几个键时,系统重启,那么可以在/etc/inittab中写入,
Quote: ca::ctrlaltdel:/sbin/shutdown -t5 -rnow
respawn,这个指定的进程将被重启,任何时候当它退出时。这意味着你没有办法结束它,除非init自己结束了。例如,
Quote: c1:1235:respawn:/sbin/agetty 38400 tty1 linux
这一行的意思非常简单,就是系统运行在级别1,2,3,5时,将默认执行/sbin/agetty程序(这个类似于上面提到的getty程序),这个程序非常有意思,就是无论什么时候它退出,init将再次启动它。这个有几个比较有意思的问题:
在slackware12.0下,当你把默认运行级别修改为4的时候,只有第6个终端可以用。原因是什么呢?因为类似上面的配置,因为那里只有1235,而没有4,这意味着当系统运行在第4级别时,其他终端下的/sbin/agetty没有启动。所以,如果想让其他终端都可以用,把1235修改为12345即可。
另外一个有趣的问题就是:正是init程序在读取这个配置行以后启动了/sbin/agetty,这就是我们的/sbin/agetty的秘密。
还有一个问题:无论我们退出哪个终端,那个"XXXlogin:"总是会被打印,原因是respawn动作有趣的性质,因为它告诉init,无论/sbin/agetty什么时候退出,重新把它启动起来,那跟"XXXlogin:"有什么关系呢?从前面的内容,我们发现正是/sbin/getty(同agetty)启动了/bin/login,而/bin/login有启动了/bin/bash,即我们的命令行程序。
而init程序作为“万物之王”,它是所有进程的“父”(也可能是祖父……)进程,那意味着其他进程最多只能是它的儿子进程。而这个子进程是怎么创建的,fork调用,而不是之前提到的execve调用。前者创建一个子进程,后者则会覆盖当前进程。因为我们发现/sbin/getty运行时,init并没有退出,因此可以判断是fork调用创建一个子进程后,才通过execve执行了/sbin/getty。
因此,我们可以总结出这么一个调用过程:
Quote: fork execve execve fork execve
init --> init --> /sbin/getty--> /bin/login --> /bin/login--> /bin/bash
这里的execve调用以后,后者将直接替换前者,因此当我们键入exit退出/bin/bash以后,也就相当于/sbin/getty都已经结束了,因此最前面的init程序判断/sbin/getty退出了,又会创建一个子进程把/sbin/getty启动,进而又启动了/bin/login,又看到了那个"XXX login:"。
通过ps和pstree命令看看实际情况是不是这样,前者打印出进程的信息,后者则打印出调用关系。
Quote: $ ps -ef | egrep"/sbin/init|/sbin/getty|bash|/bin/login"
root 1 0 0 21:43? 00:00:01 /sbin/init
root 3957 1 0 21:43tty4 00:00:00 /sbin/getty 38400 tty4
root 3958 1 0 21:43tty5 00:00:00 /sbin/getty 38400 tty5
root 3963 1 0 21:43tty3 00:00:00 /sbin/getty 38400 tty3
root 3965 1 0 21:43tty6 00:00:00 /sbin/getty 38400 tty6
root 7023 1 0 22:48tty1 00:00:00 /sbin/getty 38400 tty1
root 7081 1 0 22:51tty2 00:00:00 /bin/login--
falcon 7092 7081 0 22:52tty2 00:00:00 -bash
我们过滤了一些不相干的数据。从上面的结果可以看到,除了tty2被替换成/bin/login外,其他终端都运行着/sbin/getty,说明终端2上的进程是/bin/login,它已经把/sbin/getty替换掉,另外,我们看到-bash进程的父进程是7081刚好是/bin/login程序,这说明/bin/login启动了-bash,但是它并没有替换掉/bin/login,而是成为了/bin/login的子进程,这说明/bin/login通过fork创建了一个子进程并通过execve执行了-bash(后者通过strace跟踪到)。而init呢,其进程ID是1,是/sbin/getty和/bin/login的父进程,说明init启动或者间接启动了它们。下面通过pstree来查看调用树,更清晰的看出上述关系。
Quote: $ pstree | egrep"init|getty|\-bash|login"
init-+-5*[getty]
|-login---bash
|-xfce4-terminal-+-bash-+-grep
结果显示init是5个getty程序,login程序和xfce4-terminal的父进程,而后两者则是bash的父进程,另外我们执行的grep命令则在bash上运行,是bash的子进程,这个将是我们后面关心的问题。
从上面的结果发现,init作为所有进程的父进程,它的父进程ID饶有兴趣的是0,它是怎么被启动的呢?谁才是真正的“造物主”?
1.2.4 谁启动了/sbin/init
如果用过Lilo或者Grub这两个操作系统引导程序,你可能会用到Linux内核的一个启动参数init,当你忘记密码时,可能会把这个参数设置成/bin/bash,让系统直接进入命令行,而无须输入帐号和密码,这样就可以方便地登录密码修改掉。
这个init参数是个什么东西呢?通过manbootparam会发现它的秘密,init参数正好指定了内核启动后要启动的第一个程序,而如果没有指定该参数,内核将依次查找/sbin/init,/etc/init, /bin/init,/bin/sh,如果找不到这几个文件中的任何一个,内核就要恐慌(panic)了,并呆在那里一动不动了。
因此/sbin/init就是Linux内核启动的。而Linux内核呢?是通过Lilo或者Grub等引导程序启动的,Lilo和Grub都有相应的配置文件,一般对应/etc/lilo.conf和/boot/grub/menu.lst,通过这些配置文件可以指定内核映像文件、系统根目录所在分区、启动选项标签等信息,从而能够让它们顺利把内核启动起来。
那Lilo和Grub本身又是怎么被运行起来的呢?还记得以前介绍的MBR不?MBR就是主引导扇区,一般情况下这里存放着Lilo和Grub的代码,而谁知道正好是这里存放了它们呢?BIOS,如果你用光盘安装过操作系统的话,那么应该修改过BIOS的默认启动设置,通过设置你可以让系统从光盘、硬盘甚至软盘启动。正是这里的设置让BIOS知道了MBR处的代码需要被执行。
那BIOS又是什么时候被起来的呢?加电自检就执行到了这里。
更多系统启动的细节,看看"man boot-scripts" 吧。
到这里,/bin/bash的神秘面纱就被揭开了,它只是系统启动后运行的一个程序而已,只不过这个程序可以响应用户的请求,那它到底是如何响应用户请求的呢?
1.3 /bin/bash如何处理用户键入的命令
1.3.0 预备知识
在执行磁盘上某个程序时,我们通常不会指定这个程序文件的绝对路径,比如要执行echo命令时,我们一般不会输入/bin/echo,而仅仅是输入echo。那为什么这样bash也能够找到/bin/echo呢?原因是Linux操作系统支持这样一种策略:shell的一个环境变量PATH里头存放了程序的一些路径,当shell执行程序时有可能去这些目录下查找。which作为shell(这里特指bash)的一个内置命令,如果用户输入的命令是磁盘上的某个程序,它会返回这个文件的全路径。
有三个东西和终端的关系很大,那就是标准输入、标准输出和标准错误,它们是三个文件描述符,一般对应描述符0,1,2。在C语言程序里头,我们可以把它们当作文件描述符一样进行操作。在命令行下,则可以使用重定向字符>,<等对它们进行操作。对于标准输出和标准错误,都默认输出到终端,对于标准输入,也同样默认从终端输入。
1.3.1 哪种命令先被执行
在C语言里头要写一段输入字符串的命令很简单,调用scanf或者fgets就可以。这个在bash里头应该是类似的。但是它获取用户的命令以后,如何分析命令,如何响应不同的命令呢?
首先来看看bash下所谓的命令,用最常见的test来作测试。
Quote: $ test1 #随便键入一个字符串test1,bash发出响应,告诉我们找不到这个程序
bash: test1: command not found
$ test #当我们键入test的时候,看不到任何输出,唯一的响应是,新的命令提示符被打印了
$ type test
test is a shell builtin
#查看test这个命令的类型,即查看test将被如何解释,type告诉我们test是一个内置命令,如果没有理解错,test应该是利用诸如case"test": do something;break;这样的机制实现的。
$ which test #通过which查到/usr/bin下有一个test命令文件,在键入test时,到底哪一个被执行了呢?
/usr/bin/test
$ /usr/bin/test #执行这个呢?也没什么反应,到底谁先被执行了?
从上面的演示中发现一个问题?如果输入一个命令,这个命令要么就不存在,要么可能同时是shell的内置命令、也有可能是磁盘上环境变量PATH所指定的目录下的某个程序文件。
考虑到test内置命令和/usr/bin/test命令的响应结果一样,我们无法知道哪一个先被执行了,怎么办呢?把/usr/bin/test替换成一个我们自己的命令,并让它打印一些信息(比如hello,world!),这样我们就知道到底谁被执行了。写完程序,编译好,命名为test放到/usr/bin下(记得备份原来那个)。开始测试:
Quote: $ test #键入test,还是没有效果
$ /usr/bin/test #而键入绝对路径呢,则打印了hello, world!诶,那默认情况下肯定是内置命令先被执行了
hello, world!
总结:内置命令比磁盘文件中的程序优先被bash执行
下面看看更多有趣的东西,键盘键入的命令还有可能是什么呢?因为bash支持别名和函数,所以还有可能是别名和函数,另外,如果PATH环境变量指定的不同目录下有相同名字的程序文件,那到底哪个被优先找到呢?
下面再作一些实验,
Quote: $ alias test="ls-l" #把test命名为ls -l的别名
$ test #再执行test,竟然执行了ls -l,而不是什么也没有,说明alias比内置命令更优先
total 9488
drwxr-xr-x 12 falconfalcon 40962008-02-21 23:43 bash-3.2
-rw-r--r-- 1 falcon falcon 2529838 2008-02-2123:30 bash-3.2.tar.gz
$ function test { echo "hi, I'm a function";} #定义一个名叫test的函数
$ test #执行一下,发现,还是执行了ls-l,说明function没有alias优先级高
total 9488
drwxr-xr-x 12 falconfalcon 40962008-02-21 23:43 bash-3.2
-rw-r--r-- 1 falcon falcon 2529838 2008-02-2123:30 bash-3.2.tar.gz
$ unalias test #把别名给去掉
$ test #现在执行的是函数,说明函数的优先级比内置命令也要高
hi, I'm a function
$ builtin test #如果在命令之前跟上builtin,那么将直接执行内置命令
$ unset test #要去掉某个函数的定义,这样就可以
通过这个实验我们得到一个命令的别名(alias)、函数(function),内置命令(builtin)和程序(program)的执行优先次序:
Quote: 先 alias --> function --> builtin--> program 后
实际上,type命令会告诉我们这些细节,type -a会按照bash解析的顺序依次打印该命令的类型,而type-t则会给出第一个将被解析的命令的类型,之所以要做上面的实验,是为了让大家加印象。
Quote: $ type -a test
test is a shell builtin
test is /usr/bin/test
$ alias test="ls -l"
$ function test { echo "I'm a function"; }
$ type -a test
test is aliased to `ls -l'
test is a function
test ()
{
echo "I'm afunction"
}
test is a shell builtin
test is /usr/bin/test
$ type -t test
alias
下面再看看PATH指定的多个目录下有同名程序的情况。再写一个程序,打印“hi, world!”,以示和"hello,world!"的区别,放到PATH指定的另外一个目录/bin下,为了保证测试的说服力,再写一个放到另外一个叫/usr/local/sbin的目录下。
先看看PATH环境变量,确保它有/usr/bin,/bin和/usr/local/sbin这几个目录,然后通过type-P(-P参数强制到PATH下查找,而不管是别名还是内置命令等,可以通过helptype查看该参数的含义)查看,到底哪个先被执行。
Quote: $ echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games
$ type -P test #可以看到/usr/local/sbin下的先被找到
/usr/local/sbin/test
$ rm /usr/local/sbin/test #把/usr/local/sbin/test下的给删除掉
$ type -P test #现在/usr/bin下的先被找到
/usr/bin/test
$ type -a test #type -a也显示类似的结果
test is aliased to `ls -l'
test is a function
test ()
{
echo "I'm afunction"
}
test is a shell builtin
test is /usr/bin/test
test is /bin/test
可以找出这么一个规律:shell从PATH列出的路径中依次查找用户输入的命令。考虑到程序的优先级最低,如果想优先执行磁盘上的程序文件test呢?那么就可以用test-P找出这个文件并执行就可以了。
补充:对于shell的内置命令,可以通过helpcommand的方式获得帮助,对于程序文件,可以查看用户手册(当然,这个需要安装,一般叫做xxx-doc),mancommand。关于用户手册安装办法见在Linux下学会查看Man文档
1.3.2 这些特殊字符是如何解析的:|, >, <,&
在命令行上,除了输入各种命令以及一些参数外,比如上面type命令的各种参数-a,-P等,对于这些参数,是传递给程序本身的,非常好处理,比如if,else条件分之或者switch,case都可以处理。当然,在bash里头可能使用专门的参数处理函数getopt和getopt_long来处理它们。
而|,>,<,&等字符,则比较特别,shell是怎么处理它们的呢?它们也被传递给程序本身吗?可我们的程序内部一般都不处理这些字符的,所以应该是shell程序自己解析了它们。
先来看看这几个字符在命令行的常见用法,
Quote: $ cat <./test.c #<字符表示:把test.c文件重定向为标准输入,作为cat命令的输入,而cat默认把内容输出到标准输出。
#include <stdio.h>
int main(void)
{
printf("hi, myself!\n");
return 0;
}
$ cat < ./test.c > test_new.c#>表示把标准输出重定向为文件test_new.c,结果内容输出到test_new.c
对于>,<,>>,<<,<>我们都称之为重定向(redirect),shell到底是怎么进行所谓的“重定向”的呢?
这主要归功于dup/fcntl等函数,它们可以实现:复制文件描述符,让多个文件描述符共享同一个文件表项。比如,当把文件test.c重定向为标准输入时。假设之前用以打开test.c的文件描述符是5,现在就把5复制为了0,这样当cat试图从标准输入读出内容时,也就访问了文件描述符5指向的文件表项,接着读出了文件内容。输出重定向与此类似。其他的重定向,诸如>>,<<,<>等虽然和>,<的具体实现功能不太一样,但本质是一样的,都是文件描述符的复制,只不过可能对文件操作有一些附加的限制,比如>>在输出时追加到文件末尾,而>则会从头开始写入文件,前者意味着文件的大小会增长,而后者则意味文件被重写。
那么|呢?"|"被形象地称为“管道”,实际上它就是通过C语言里头的无名管道来实现的。先看一个例子,
Quote: $ cat <./test.c | grep hi
printf("hi, myself!\n");
在这个例子中,cat读出了test.c文件中的内容,并输出到标准输出上,但是实际上输出的内容却只有一行,原因是这个标准输出被“接到”了grep命令的标准输入上,而grep命令只打印了包含“hi”字符串的一行。
这是怎么被“接”上的。cat和grep作为两个单独的命令,它们本身没有办法把两者的输入和输出“接”起来。这正是shell自己的“杰作”,它通过C语言里头的pipe函数创建了一个管道(一个包含两个文件描述符的整形数组,一个描述符用于写入数据,一个描述符用于读入数据),并且通过dup/fcntl把cat的输出复制到了管道的输入,而把管道的输出则复制到了grep的输入。这真是一个奇妙的想法。
那&呢?当你在程序的最后跟上这个奇妙的字符以后就可以接着做其他事情了,看看效果,
Quote: $ sleep 50& #让程序在后台运行
[1] 8261
$ fg %1 #提示符被打印出来,可以输入东西,让程序到前台运行,无法输入东西了,按下CTRL+Z,再让程序到后台运行
sleep 50
[1]+ Stopped sleep 50
$ fg %1 #再调到前台
sleep 50
实际上&正是shell支持作业控制的表征,通过作业控制,用户在命令行上可以同时作几个事情(把当前不做的放到后台,用&或者CTRL+Z或者bg)并且可以自由的选择当前需要执行哪一个(用fg调到前台)。这在实现时应该涉及到很多东西,包括终端会话(session)、终端信号、前台进程、后台进程等。而在命令的后面加上&后,该命令将被作为后台进程执行,后台进程是什么呢?这类进程无法接收用户发送给终端的信号(如SIGHUP,SIGQUIT,SIGINT),无法响应键盘输入(被前台进程占用着),不过可以通过fg切换到前台而享受作为前台进程具有的特权。
因此,当一个命令被加上&执行后,shell必须让它具有后台进程的特征,让它无法响应键盘的输入,无法响应终端的信号(意味忽略这些信号),并且比较重要的是新的命令提示符得打印出来,并且让命令行接口可以继续执行其他命令,这些就是shell对&的执行动作。
还有什么神秘的呢?你也可以写自己的shell了,并且可以让内核启动后就执行它l,在lilo或者grub的启动参数上设置init=/path/to/your/own/shell/program就可以。当然,也可以把它作为自己的登录shell,只需要放到/etc/passwd文件中相应用户名所在行的最后就可以。不过貌似到现在还没介绍shell是怎么执行程序,是怎样让程序变成进程的,所以继续。
1.3.3 /bin/bash用什么魔法让一个普通程序变成了进程
当我们从键盘键入一串命令,shell奇妙的响应了,对于内置命令和函数,shell自身就可以解析了(通过switchcase之类的C语言语句)。但是,如果这个命令是磁盘上的一个文件呢。它找到该文件以后,怎么执行它的呢?
还是用strace来跟踪一个命令的执行过程看看。
Quote: $ strace -f -o strace.log /usr/bin/test
hello, world!
$ cat strace.log | sed -ne "1p" #我们对第一行很感兴趣
8445 execve("/usr/bin/test", ["/usr/bin/test"],[]) = 0
从跟踪到的结果的第一行可以看到bash通过execve调用了/usr/bin/test,并且给它传了33个参数。这33个vars是什么呢?看看declare -x的结果(这个结果只有32个,原因是vars的最后一个变量需要是一个结束标志,即NULL)。
Quote: $ declare -x | wc-l #declare-x声明的环境变量将被导出到子进程中
32
$ export TEST="just a test" #为了认证declare -x和之前的vars的个数的关系,再加一个
$ declare -x | wc -l
33
$ strace -f -o strace.log/usr/bin/test #再次跟踪,看看这个关系
hello, world!
$ cat strace.log | sed -ne"1p"
8523 execve("/usr/bin/test", ["/usr/bin/test"],[]) = 0
通过这个演示发现,当前shell的环境变量中被设置为export的变量被复制到了新的程序里头。不过虽然我们认为shell执行新程序时是在一个新的进程里头执行的,但是strace并没有跟踪到诸如fork的系统调用(可能是strace自己设计的时候并没有跟踪fork,或者是在fork之后才跟踪)。但是有一个事实我们不得不承认:当前shell并没有被新程序的进程替换,所以说shell肯定是先调用fork(也有可能是vfork)创建了一个子进程,然后再调用execve执行新程序的。如果你还不相信,那么直接通过exec执行新程序看看,这个可是直接把当前shell的进程替换掉的。
Quote: exec /usr/bin/test
应该可以看到当前shell“哗”(听不到,突然没了而已)的一下就没有了。
下面来模拟一下shell执行普通程序。multiprocess相当于当前shell,而/usr/bin/test则相当于通过命令行传递给shell的一个程序。这里是代码:
Code:
[Ctrl+A Select All]
运行看看,
Quote: $ make multiprocess
$ ./multiprocess
child: my pid is 2251
child: my parent's pid is 2250
hello, world!
parent: my pid is 2250
parent: wait for my child exit successfully!
从执行结果可以看出,/usr/bin/test在multiprocess的子进程中运行并不干扰父进程,因为父进程一直等到了/usr/bin/test执行完成。
再回头看看代码,你会发现execlp并没有传递任何环境变量信息给/usr/bin/test,到底是怎么把环境变量传送过去的呢?通过manexec我们可以看到一组exec的调用,在里头并没有发现execve,但是通过manexecve可以看到该系统调用。实际上exec的那一组调用都只是libc库提供的,而execve才是真正的系统调用,也就是说无论使用exec调用中的哪一个,最终调用的都是execve,如果使用execlp,那么execlp将通过一定的处理把参数转换为execve的参数。因此,虽然我们没有传递任何环境变量给execlp,但是默认情况下,execlp把父进程的环境变量复制给了子进程,而这个动作是在execlp函数内部完成的。
现在,总结一下execve,它有有三个参数,
第一个是程序本身的绝对路径,对于刚才使用的execlp,我们没有指定路径,这意味着它会设法到PATH环境变量指定的路径下去寻找程序的全路径。
第二个参数是一个将传递给被它执行的程序的参数数组指针。正是这个参数把我们从命令行上输入的那些参数,诸如grep命令的-v等传递给了新程序,可以通过main函数的第二个参数char*argv[]获得这些内容。
第三个参数是一个将传递给被它执行的程序的环境变量,这些环境变量也可以通过main函数的第三个变量获取,只要定义一个char*env[]就可以了,只是通常不直接用它罢了,而是通过另外的方式,通过extern char**environ全局变量(环境变量表的指针)或者getenv函数来获取某个环境变量的值。
当然,实际上,当程序被execve执行后,它被加载到了内存里,包括程序的各种指令、数据以及传递给它的各种参数、环境变量等都被存放在系统分配给该程序的内存空间中。