1.指令
cp指令✔
在Windows中我们可以通过Ctrl+C和Ctrl+V将一个地方的文件或目录复制一份到另一个地方,我们若是在Linux当中想要完成该操作,就要用到cp指令。
语法: cp 选项 源文件或目录 目标文件或目录
功能: 复制文件或目录。
mv指令✔
在Windows中我们经常使用Ctrl+X和Ctrl+V将一个地方的文件或目录移动到另一个地方,我们若是要在Linux当中完成此操作,则需要使用mv指令。
语法: mv 选项 源文件或目录 目标文件或目录
功能: 视mv指令当中两个参数的不同而不同。
cat指令✔
在Linux当中无法想Windows一样用鼠标点开某一文件进行阅读,要想阅读文件可以用cat指令。
语法: cat 选项 文件
功能: 查看目标文件的内容。
less指令✔
上面的more指令只能进行下翻操作,而less指令可以同时支持上翻和下翻操作。
语法: less 选项 文件
功能: 既有查看目标文件的内容的功能,又有搜索功能。
head指令✔
语法: head 选项 文件
功能: 用于显示文件的开头至标准输出中,默认head指令打印其相应文件的开头10行。

find指令✔
语法: find 路径 选项 ...
功能: 用于在文件树中查找文件,并作出相应的处理(可能访问磁盘->效率比较低)。
常用选项:
1)-name 按照文件名查找文件。
zip/unzip指令✔
zip指令用于打包压缩目录或文件。
语法: zip 选项 压缩文件.zip 待打包目录或文件
功能: 将目录或文件打包压缩成zip格式。
常用选项:
1)-r 递归处理,将指定目录下的所有文件和子目录一并处理。

unzip指令用于解压文件。
语法: unzip 压缩文件.zip 选项 目录
功能: 将压缩文件解压到指定目录当中。

tar指令✔
tar指令也可以用来打包压缩和解压文件,并且还可以实现不解开压缩包,直接查看里面内容的操作,以及只打包不压缩等操作。
语法: tar 选项 压缩文件 ...
功能: 打包/解包,不解开压缩包,直接查看里面内容。


file指令✔
语法: file
功能: 查看文件的更多属性信息。
Shell
我们都知道Windows以图形化界面为交互方式,而Linux以命令行界面为交互方式。Windows和Linux的交互方式虽然不同,但本质上是一样的,图形化界面和命令行界面都是为了让用户进行相关操作,而图形化界面和命令行界面就是我们所说的"外壳程序" 。有命令行shell和图形化界面shell。
Linux当中的权限
Linux权限的概念
在Linux下有两种用户,分别是超级用户(root)和普通用户。超级用户可以在Linux下做任何事情,几乎不受限制,而普通用户一般只能在自己的工作目录下(/home/xxx)工作,以及在系统上做有限的工作。
换句话来说,所有的权限的概念都是用来限制普通用户的,而超级用户几乎不受限制。
Linux权限管理
文件访问者的分类(人)
对于用户来说,权限可以将用户分为三大类:
1)文件和文件目录的所有者(文件拥有者 )。
2)文件拥有者所在的组的用户(文件所属组 )。
3)其他用户(other)。
文件类型和访问权限(事物属性)
权限涉及到某个具体的事物来说,我们还需要讨论事物本身的属性。对于文件来说,我们应该讨论其文件类型,以及是否具有可读、可写和可执行的属性。
使用指令ll,我们可以看到前面有一串字符,这串字符实际上就代表着该文件的类型和属性。

文件权限值的表示方法
a)字符表示方法
ll指令打印文件权限值时的表示方法就是字符表示法。
2)八进制数值表示法
字符表示法中的每一个字符所在位置所表示的结果只有两种可能,要么为真,要么为假,因此我们可以将这三个字符换为三个二进制位,进而换为一个八进制位进行表示。
文件访问权限的相关设置方法

如何修改文件的掩码
我们查看新建的文件和目录,它们都有自己默认的权限。新建文件的默认权限为0666,新建目录的默认权限为0777。其中第一位的0与特殊权限有关,我们这里不必深究,而后面三位就是权限的八进制数值表示方法,我们将其翻译为字符表示方法。umake默认是0002。
目录的权限
对于文件来说,其可读可写可执行的属性我们都知道分别代表着什么对应的操作,那对于目录来说可读可写可执行又分别代表着什么呢?
1)可读权限: 如果用户没有该目录的可读权限,则无法通过ls指令查看目录中的文件内容。
2)可写权限: 如果用户没有该目录的可写权限,则无法通过一系列指令在目录中创建文件或删除文件。
3)可执行权限: 如果用户没有该目录的可执行权限,则无法通过cd指令进入到目录当中。
那么这就会出现一个问题:
只要用户拥有某目录的可写权限,就可以删除该目录当中的文件,而不论该用户是否拥有该文件的可写权限,这显然是不合理的。
为了解决这个不合理的问题,Linux引入了粘滞位的概念。
粘滞位
语法: chmod +t 目录名
功能: 给目录加上粘滞位。
当一个用户将某一个目录加上粘滞位后,该目录的权限值的最后一位变为字符"t"。就是其余人只能在下面创建文件,而不能删除文件。
gcc/g++语法
语法: gcc/g++ 选项 文件
常用选项:
1)-E 只进行预处理,这个不生成文件,你需要把他重定向到一个输出文件里面(否则将把预处理后的结果打印到屏幕上)。
2)-S 编译到汇编语言,不进行汇编和链接,即只进行预处理和编译。
3)-c 编译到目标代码
4)-o 将处理结果输出到指定文件,该选项后需紧跟输出文件名。
5)-static 此选项对生成的文件采用静态链接。
6)-g 生成调试信息(若不携带该选项则默认生成release版本)。
Linux调试器 - gdb
程序发布方式:
1、debug版本:程序本身会被加入更多的调试信息,以便于进行调试。
2、release版本:不会添加任何调试信息,是不可调试的。
在Linux当中gcc/g++默认生成的可执行程序是release版本的,是不可被调试的。如果想生成debug版本,就需要在使用gcc/g++生成可执行程序时加上-g选项。

Linux项目自动化构建工具 - make/Makefile
make是一条命令,Makefile是一个文件,两个搭配使用,完成项目自动化构建。
- Makefile文件的简写方式:
- $@:表示依赖关系中的目标文件(冒号左侧)。
- $^:表示依赖关系中的依赖文件列表(冒号右侧全部)。
- $<:表示依赖关系中的第一个依赖文件(冒号右侧第一个)。

make原理
- make会在当前目录下找名字为"Makefile"或"makefile"的文件。
- 如果找到,它会找文件当中的第一个目标文件,在上面的例子中,它会找到mytest这个文件,并把这个文件作为最终的目标文件。
- 如果mytest文件不存在,或是mytest所依赖的后面的test.o文件和main.o文件的文件修改时间比mytest文件新,那么它就会执行后面的依赖方法来生成mytest文件
- 如果mytest所依赖的test.o文件不存在,那么make会在Makefile文件中寻找目标为test.o文件的依赖关系,如果找到则再根据其依赖方法生成test.o文件(类似于堆栈的过程)。
- 当然,你的test.c文件和main.c文件是存在的,于是make会生成test.o文件和main.o文件,然后再用test.o文件和main.o文件生成最终的mytest文件。
- make会一层又一层地去找文件的依赖关系,直到最终编译出第一个目标文件。
- 在寻找的过程中,如果出现错误,例如最后被依赖的文件找不到,那么make就会直接退出,并报错。
Linux进程概念
基本概念
课本概念: 程序的一个执行实例,正在执行的程序等。
内核观点: 担当分配系统资源(CPU时间,内存)的实体。
当你的代码进行编译链接后便会生成一个可执行程序,这个可执行程序本质上是一个文件,是放在磁盘上的。当我们双击这个可执行程序将其运行起来时,本质上是将这个程序加载到内存当中了,因为只有加载到内存后,CPU才能对其进行逐行的语句执行,而一旦将这个程序加载到内存后,我们就不应该将这个程序再叫做程序了,严格意义上将应该将其称之为进程。
描述进程-PCB
系统当中可以同时存在大量进程,使用命令ps aux便可以显示系统当中存在的进程。

你开机的时候启动的第一个程序就是我们的操作系统(即操作系统是第一个加载到内存的),我们都知道操作系统是做管理工作的,而其中就包括了进程管理。而系统内是存在大量进程的,那么操作系统是如何对进程进行管理的呢?
这时我们就应该想到管理的六字真言:先描述,再组织。操作系统管理进程也是一样的,操作系统作为管理者是不需要直接和被管理者(进程)直接进行沟通的,当一个进程出现时,操作系统就立马对其进行描述,之后对该进程的管理实际上就是对其描述信息的管理。
进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合,课本上称之为PCB,操作系统将每一个进程都进行描述,形成了一个个的进程控制块(PCB),并将这些PCB以双链表的形式组织起来。

总的来说,操作系统对进程的管理实际上就变成了对该双链表的增、删、查、改等操作。
task_struct-PCB的一种
Linux操作系统是用C语言进行编写的,那么Linux当中的进程控制块必定是用结构体来实现的。
PCB实际上是对进程控制块的统称,在Linux中描述进程的结构体叫做task_struct。
task_struct是Linux内核的一种数据结构,它会被装载到RAM(内存)里并且包含进程的信息。
task_struct内容分类
task_struct就是Linux当中的进程控制块,task_struct当中主要包含以下信息:
- 标示符: 描述本进程的唯一标示符,用来区别其他进程。
- 状态: 任务状态,退出代码,退出信号等。
- 优先级: 相对于其他进程的优先级。
- 程序计数器(pc): 程序中即将被执行的下一条指令的地址。
- 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针。
- 上下文数据: 进程执行时处理器的寄存器中的数据。
查看进程
通过系统目录查看
在根目录下有一个名为proc的系统文件夹。文件夹当中包含大量进程信息,其中有些子目录的目录名为数字。

通过ps命令查看
单独使用ps命令,会显示所有进程信息。
bash
ps -aux
ps命令与grep命令搭配使用,即可只显示某一进程的信息。
cpp
$ ps aux | head -1 && ps aux | grep proc | grep -v grep
如下:

通过系统调用获取进程的PID和PPID
通过使用系统调用函数,getpid和getppid即可分别获取进程的PID和PPID。
我们可以通过一段代码来进行测试。

通过系统调用创建进程- fork初始
fork是一个系统调用级别的函数,其功能就是创建一个子进程。每出现一个进程,操作系统就会为其创建PCB,fork函数创建的进程也不例外。
fork函数的返回值:
1、如果子进程创建成功,在父进程中返回子进程的PID,而在子进程中返回0。
2、如果子进程创建失败,则在父进程中返回 -1。
既然父进程和子进程获取到fork函数的返回值不同,那么我们就可以据此来让父子进程执行不同的代码,从而做不同的事。

Linux进程状态
一个进程从创建而产生至撤销而消亡的整个生命期间,有时占有处理器执行,有时虽可运行但分不到处理器,有时虽有空闲处理器但因等待某个时间的发生而无法执行,这一切都说明进程和程序不相同,进程是活动的且有状态变化的,于是就有了进程状态这一概念。
僵尸进程
前面说到,一个进程若是正在等待其退出信息被读取,那么我们称该进程处于僵尸状态。而处于僵尸状态的进程,我们就称之为僵尸进程。
例如,对于以下代码,fork函数创建的子进程在打印5次信息后会退出,而父进程会一直打印信息。也就是说,子进程退出了,父进程还在运行,但父进程没有读取子进程的退出信息,那么此时
僵尸进程的危害
- 僵尸进程的退出状态必须一直维持下去,因为它要告诉其父进程相应的退出信息。可是父进程一直不读取,那么子进程也就一直处于僵尸状态。
- 僵尸进程的退出信息被保存在task_struct(PCB)中,僵尸状态一直不退出,那么PCB就一直需要进行维护。
- 若是一个父进程创建了很多子进程,但都不进行回收,那么就会造成资源浪费,因为数据结构对象本身就要占用内存。
- 僵尸进程申请的资源无法进行回收,那么僵尸进程越多,实际可用的资源就越少,也就是说,僵尸进程会导致内存泄漏。
孤儿进程
在Linux当中的进程关系大多数是父子关系,若子进程先退出而父进程没有对子进程的退出信息进行读取,那么我们称该进程为僵尸进程。但若是父进程先退出,那么将来子进程进入僵尸状态时就没有父进程对其进行处理,此时该子进程就称之为孤儿进程。
若是一直不处理孤儿进程的退出信息,那么孤儿进程就会一直占用资源,此时就会造成内存泄漏。因此,当出现孤儿进程的时候,孤儿进程会被1号init进程领养,此后当孤儿进程进入僵尸状态时就由int进程进行处理回收。
进程优先级
优先级存在的原因?
优先级存在的主要原因就是资源是有限的,而存在进程优先级的主要原因就是CPU资源是有限的,一个CPU一次只能跑一个进程,而进程是可以有多个的,所以需要存在进程优先级,来确定进程获取CPU资源的先后顺序。

注意: 在Linux操作系统当中,PRI(old)默认为80,即PRI = 80 + NI。NI的取值范围是-20至19,一共40个级别。
通过renice命令更改进程的nice值
使用renice命令,后面跟上更改后的nice值和进程的PID即可。
cpp
renice 10 15587
环境变量
环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数。
常见环境变量
- PATH: 指定命令的搜索路径。
- HOME: 指定用户的主工作目录(即用户登录到Linux系统中的默认所处目录)。
- SHELL: 当前Shell,它的值通常是/bin/bash。
查看环境变量的方法
我们可以通过echo命令来查看环境变量,方式如下:
echo $NAME //NAME为待查看的环境变量名称
为什么执行ls命令的时候不用带./就可以执行,而我们自己生成的可执行程序必须要在前面带上./才可以执行?
系统就是通过环境变量PATH来找到ls命令的,查看环境变量PATH我们可以看到如下内容:

可以看到环境变量PATH当中有多条路径,这些路径由冒号隔开,当你使用ls命令时,系统就会查看环境变量PATH,然后默认从左到右依次在各个路径当中进行查找。
而ls命令实际就位于PATH当中的某一个路径下,所以就算ls命令不带路径执行,系统也是能够找到的。
和环境变量相关的命令
1、echo:显示某个环境变量的值。
2、export:设置一个新的环境变量。

3、env:显示所有的环境变量。
环境变量的组织方式
在系统当中,环境变量的组织方式如下:每个程序都会收到一张环境变量表,环境表是一个字符指针数组,每个指针指向一个以'\0'结尾的环境字符串,最后一个字符指针为空。

通过代码获取环境变量
你知道main函数其实是有参数的吗?
main函数其实有三个参数,只是我们平时基本不用它们,所以一般情况下都没有写出来。
我们可以在Windows下的编译器进行验证,当我们调试代码的时候,若是一直使用逐步调试,那么最终会来到调用main函数的地方。
在这里我们可以看到,调用main函数时给main函数传递了三个参数。
main函数的第二个参数是一个字符指针数组,**数组当中的第一个字符指针存储的是可执行程序的位置,其余字符指针存储的是所给的若干选项,**最后一个字符指针为空,而main函数的第一个参数代表的就是字符指针数组当中的有效元素个数。
现在我们来说说main函数的第三个参数。
main函数的第三个参数接收的实际上就是环境变量表,我们可以通过main函数的第三个参数来获取系统的环境变量。

libc中定义的全局变量environ指向环境变量表,environ没有包含在任何头文件中,所以在使用时要用extern进行声明。
通过系统调用获取环境变量
除了通过main函数的第三个参数和第三方变量environ来获取环境变量外,我们还可以通过系统调用getenv函数来获取环境变量。getenv函数可以根据所给环境变量名,在环境变量表当中进行搜索,并返回一个指向相应值的字符串指针。
例如,使用getenv函数获取环境变量PATH的值。

程序地址空间
子进程需要将全局变量g_val改为200,那么此时就在内存的某处存储g_val的新值,并且改变子进程当中g_val的虚拟地址通过页表映射后得到的物理地址即可。

为什么要有进程地址空间?
1、有了进程地址空间后,就不会有任何系统级别的越界问题存在了。例如进程1不会错误的访问到进程2的物理地址空间,因为你对某一地址空间进行操作之前需要先通过页表映射到物理内存,而页表只会映射属于你的物理内存。总的来说,虚拟地址和页表的配合使用,本质功能就是包含内存。
2、有了进程地址空间后,每个进程都认为看得到都是相同的空间范围,包括进程地址空间的构成和内部区域的划分顺序等都是相同的,这样一来我们在编写程序的时候就只需关注虚拟地址,而无需关注数据在物理内存当中实际的存储位置。
3、有了进程地址空间后,每个进程都认为自己在独占内存,这样能更好的完成进程的独立性以及合理使用内存空间(当实际需要使用内存空间的时候再在内存进行开辟),并能将进程调度与内存管理进行解耦或分离。
对于创建进程的现阶段理解:
一个进程的创建实际上伴随着其进程控制块(task_struct)、进程地址空间(mm_struct)以及页表的创建。
Linux2.6内核进程调度队列

一个CPU拥有一个runqueue
如果有多个CPU就要考虑进程个数的父子均衡问题。
优先级
queue下标说明:
- 普通优先级:100~139。
- 实时优先级:0~99。
我们进程的都是普通的优先级,前面说到nice值的取值范围是-20~19,共40个级别,依次对应queue当中普通优先级的下标100~139。
活动队列
时间片还没有结束的所有进程都按照优先级放在活动队列当中,其中nr_active代表总共有多少个运行状态的进程,而queue[140]数组当中的一个元素就是一个进程队列,相同优先级的进程按照FIFO规则进程排队调度。
调度过程如下:
- 从0下标开始遍历queue[140]。
- 找到第一个非空队列,该队列必定为优先级最高的队列。
- 拿到选中队列的第一个进程,开始运行,调度完成。
- 接着拿到选中队列的第二个进程进行调度,直到选中进程队列当中的所有进程都被调度。
- 继续向后遍历queue[140],寻找下一个非空队列。
bitmap[5]:queue数组当中一共有140个元素,即140个优先级,一共140个进程队列,为了提高查找非空队列的效率,就可以用5 × \times× 32个比特位表示队列是否为空,这样一来便可以大大提高查找效率。
由于活动队列上时间片未到期的进程会越来越少,而过期队列上的进程数量会越来越多(新创建的进程都会被放到过期队列上),那么总会出现活动队列上的全部进程的时间片都到期的情况,这时将active指针和expired指针的内容交换,就相当于让过期队列变成活动队列,活动队列变成过期队列,就相当于又具有了一批新的活动进程,如此循环进行即可。
Linux进程控制
一、进程创建
fork函数初识
在Linux中,fork函数是非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。
返回值:
在子进程中返回0,父进程中返回子进程的PID,子进程创建失败返回-1。
进程调用fork,当控制转移到内核中的fork代码后,内核做:
- 分配新的内存块和内核数据结构给子进程。
- 将父进程部分数据结构内容拷贝至子进程。
- 添加子进程到系统进程列表当中。
- fork返回,开始调度器调度。
fork之后,父子进程代码共享。
fork常规用法
- 一个进程希望复制自己,使子进程同时执行不同的代码段。例如父进程等待客户端请求,生成子进程来处理请求。
- 一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数。
二、进程终止
进程退出场景
进程退出只有三种情况:
- 代码运行完毕,结果正确。
- 代码运行完毕,结果不正确。
- 代码异常终止(进程崩溃)。
进程退出码
当我们的代码运行起来就变成了进程,当进程结束后main函数的返回值实际上就是该进程的进程退出码,我们可以使用echo $?命令查看最近一次进程退出的退出码信息。
为什么以0表示代码执行成功,以非0表示代码执行错误?
因为代码执行成功只有一种情况,成功了就是成功了,而代码执行错误却有多种原因,例如内存空间不足、非法访问以及栈溢出等等,我们就可以用这些非0的数字分别表示代码执行错误的原因。
我们可以看到各个错误码所对应的错误信息:

exit函数
使用exit函数退出进程也是我们常用的方法,exit函数可以在代码中的任何地方退出进程,并且exit函数在退出进程前会做一系列工作:
- 执行用户通过atexit或on_exit定义的清理函数。
- 关闭所有打开的流,所有的缓存数据均被写入。
- 调用_exit函数终止进程。
return、exit和_exit之间的区别
只有在main函数当中的return才能起到退出进程的作用,子函数当中return不能退出进程,而exit函数和_exit函数在代码中的任何地方使用都可以起到退出进程的作用。
使用exit函数退出进程前,exit函数会执行用户定义的清理函数、冲刷缓冲,关闭流等操作,然后再终止进程,而_exit函数会直接终止进程,不会做任何收尾工作。
return、exit和_exit之间的联系
执行return num等同于执行exit(num),因为调用main函数运行结束后,会将main函数的返回值当做exit的参数来调用exit函数。
进程异常退出
情况一:向进程发生信号导致进程异常退出。
例如,在进程运行过程中向进程发生kill -9信号使得进程异常退出,或是使用Ctrl+C使得进程异常退出等。
情况二:代码错误导致 进程运行时异常退出。
例如,代码当中存在野指针问题使得进程运行时异常退出,或是出现除0的情况使得进程运行时异常退出等。
三、进程等待
进程等待的必要性
- 子进程退出,父进程如果不读取子进程的退出信息,子进程就会变成僵尸进程,进而造成内存泄漏。
- 进程一旦变成僵尸进程,那么就算是kill -9命令也无法将其杀死,因为谁也无法杀死一个已经死去的进程。
- 对于一个进程来说,最关心自己的就是其父进程,因为父进程需要知道自己派给子进程的任务完成的如何。
- 父进程需要通过进程等待的方式,回收子进程资源,获取子进程的退出信息。
获取子进程status
下面进程等待所使用的两个函数wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统进行填充。

我们通过一系列位操作,就可以根据status得到进程的退出码和退出信号。
cpp
exitCode = (status >> 8) & 0xFF; //退出码
exitSignal = status & 0x7F; //退出信号
对于此,系统当中提供了两个宏来获取退出码和退出信号。
- WIFEXITED(status):用于查看进程是否是正常退出,本质是检查是否收到信号。
- WEXITSTATUS(status):用于获取进程的退出码。
进程等待的方法
wait方法
函数原型:pid_t wait(int* status);
作用:等待任意子进程。
返回值:等待成功返回被等待进程的pid,等待失败返回-1。
参数:输出型参数,获取子进程的退出状态,不关心可设置为NULL。
例如,创建子进程后,父进程可使用wait函数一直等待子进程,直到子进程退出后读取子进程的退出信息。
cpp
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
int main()
{
pid_t id = fork();
if(id == 0){
//child
int count = 10;
while(count--){
printf("I am child...PID:%d, PPID:%d\n", getpid(), getppid());
sleep(1);
}
exit(0);
}
//father
int status = 0;
pid_t ret = wait(&status);
if(ret > 0){
//wait success
printf("wait child success...\n");
if(WIFEXITED(status)){
//exit normal
printf("exit code:%d\n", WEXITSTATUS(status));
}
}
sleep(3);
return 0;
}
waitpid方法
函数原型:
pid_t waitpid(pid_t pid, int* status, int options);
作用:等待指定子进程或任意子进程。
返回值:
1、等待成功返回被等待进程的pid。
2、如果设置了选项WNOHANG
,而调用中waitpid发现没有已退出的子进程可收集,则返回0。
3、如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在。
四、进程程序替换
替换原理
用fork创建子进程后,子进程执行的是和父进程相同的程序(但有可能执行不同的代码分支),若想让子进程执行另一个程序,往往需要调用一种exec函数。
当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,并从新程序的启动例程开始执行。
替换函数
替换函数有六种以exec开头的函数,它们统称为exec函数:
二、
int execlp(const char *file, const char *arg, ...);
l代表以列表形式给指令如何执行,p代表带有环境变量的路径。第一个参数是要执行程序的名字,第二个参数是可变参数列表,表示你要如何执行这个程序,并以NULL结尾。
例如,要执行的是ls程序。
cpp
execlp("ls", "ls", "-a", "-i", "-l", NULL);
函数解释
- 这些函数如果调用成功,则加载指定的程序并从启动代码开始执行,不再返回。
- 如果调用出错,则返回-1。
也就是说,exec系列函数只要返回了,就意味着调用失败。