什么是程序呢?
其实程序就是存放在存储介质上的可执行文件,它包含一系列信息,这些信息描述了如何在运行时创建一个进程。
那有人又会问什么是进程呢?
直观的来说,我们平时写的 C/C++ 语言代码,通过编译器编译,最终它会成为一个可执行程序,当这个可执行程序运行起来后,它就成为了一个进程。
所以程序是存放在存储介质上的一个可执行文件,而进程是程序执行的过程。进程的状态是变化的,其包括进程的创建、调度和消亡。程序是静态的,进程是动态的。
打个比方:程序就类似于剧本(纸),代码就相当于剧本稿纸上文章(字),进程类似于戏(舞台、演员、灯光、道具…)。同一个剧本可以在多个舞台同时上演。同样,同一个程序也可以加载为不同的进程(彼此之间互不影响)。
示例:

理解程序与进程之后,下面来一个官方的说法:
我们可以这么理解,公司相当于操作系统,部门相当于进程,公司通过部门来管理(系统通过进程管理),对于各个部门,每个部门有各自的资源,如人员、电脑设备、打印机等。
【补充】ulimit -a 命令可以显示当前系统的一些资源的上限,也可以 ulimit 命令修改这些资源的上限。

单道程序设计
所有进程一个一个排队执行。若 A 阻塞,B 只能等待,即使 CPU 处于空闲状态,比如人机交互时阻塞的出现是必然的,与此同时 CPU 处于空闲等待状态。所以这种模型在系统资源利用上及其不合理,在计算机发展历史上存在不久,大部分便被淘汰了。
多道程序设计
在计算机内存中同时存放几道相互独立的程序,它们在管理程序控制之下,相互穿插的运行。多道程序设计必须有硬件基础作为保证。在多道程序设计模型中,两个或两个以上程序在计算机系统中同处于开始到结束之间的状态,这些程序共享计算机系统资源。引入多道程序设计技术的根本目的是为了提高 CPU 的利用率。
时间片(timeslice)又称为“量子(quantum)”或“处理器片(processor slice)”是操作系统分配给每个正在运行的进程微观上的一段 CPU 时间。
事实上,虽然一台计算机通常可能有多个 CPU,但是同一个 CPU 永远不可能真正地同时运行多个任务,程序同时处于运行状态只是一种宏观上的概念,它们虽然都已经开始运行,但就微观而言,任意时刻,CPU 上运行的程序只有一个:在只考虑一个 CPU 的情况下,这些进程“看起来像”同时运行的,实则是轮番穿插地运行,由于时间片通常很短(在 Linux 上为 5ms-800ms),用户反应不过来,所以看似同时在运行。
当下常见 CPU 为纳秒级,1 秒可以执行大约 10 亿条指令,人眼的反应速度是毫秒级。
1s = 1000ms
1ms = 1000us
1us = 1000ns
1s = 1000000000ns
时间片由操作系统内核的调度程序分配给每个进程。首先,内核会给每个进程分配相等的初始时间片,然后每个进程轮番地执行相应的时间,当所有进程都处于时间片耗尽的状态时,内核会重新为每个进程计算并分配时间片,如此往复。【注意】时间片不能太短,也不能太长:太短的话,切换成本太高,CPU要消耗大量的时间进行时间片切换;太长的话,人都有可能反应过来,宏观上感觉不到多个程序同时运行。
并行(parallel)
指在同一时刻,有多条指令在多个处理器上同时执行。
并发(concurrency)
指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行。
在计算机中,时钟中断是多道程序设计模型的理论基础。并发时,任意进程在执行期间都不希望放弃 cpu。因此系统需要一种强制让进程让出 cpu 资源的手段。时钟中断有硬件基础作为保障,对进程而言不可抗拒。 操作系统中的中断处理函数,来负责调度程序执行。
举例说明:

MMU
MMU 是 Memory Management Unit 的缩写,中文名是内存管理单元,它是中央处理器(CPU)中用来管理虚拟存储器、物理存储器的控制线路,同时也负责虚拟地址映射为物理地址,以及提供硬件机制的内存访问授权,多用户多进程操作系统。

[[03_Linux 常用 API 函数#2. 虚拟内存空间|虚拟内存空间]]
为了管理进程,内核必须对每个进程所做的事情进行清楚的描述。所以内核为每个进程分配一个PCB(进程控制块),维护进程相关的信息。Linux内核的进程控制块是task_struct 结构体。
在 /usr/src/linux-headers-xxx/include/linux/sched.h 文件中可以查看 struct task_struct 结构体的定义。其内部成员有很多,了解掌握以下部分即可:
进程状态反映进程执行过程的变化。这些状态随着进程的执行和外界条件的变化而转换。
在三态模型中,进程状态分为三个基本状态,即就绪态,运行态,阻塞态。

在五态模型中,进程分为新建态、就绪态,运行态,阻塞态,终止态。
新建态:进程刚被创建时的状态,尚未进入就绪队列;
终止态:进程完成任务到达正常结束点,或出现无法克服的错误而异常终止,或被操作系统及有终止权的进程所终止时所处的状态。进入终止态的进程以后不再执行,但依然保留在操作系统中等待善后。一旦其他进程完成了对终止态进程的信息抽取之后,操作系统将删除该进程。
除了运行态可以终止进程进入终止态之外,就绪态也可以直接到终止态,阻塞态也可以直接到终止态。

每个进程都由一个进程号来标识,其类型为 pid_t,进程号的类型—pid_t 其实为一个短整形,所以 pid_t 能表示的范围是:0~32767。进程号总是唯一的,但进程号可以重用:同一时刻,只能有一个进程使用一个进程号,当一个进程终止后,该进程号就可以再次被其他进程使用。

接下来,再给介绍三个不同的进程号。
ps 命令可以查看进程的详细状况,常用选项(选项可以不加“-”)如下:
| 选项 | 含义 |
|---|---|
| -a | 显示终端上的所有进程,包括其他用户的进程 |
| -u | 显示进程的详细状态 |
| -x | 显示没有控制终端的进程 |
| -w | 显示加宽,以便显示更多的信息 |
| -r | 只显示正在运行的进程 |
ps 常见用法:
(1)ps aux:显示这个操作系统上所有进程的信息,相当于一个拍照,不能动态显示。

(2)ps -ef:效果与 ps aux 差不多, ps aux 最初用到 Unix Style 中,而 ps -ef 被用在 System V Style 中,两者输出略有不同。现在的大部分Linux系统都是可以同时使用这两种方式的。
(3)ps ajx:以比较完整的格式显示所有的进程,会显示进程的父进程 ID、进程组 ID、会话 ID 等

(4)ps a:显示当前终端下的所有进程,包括其他用户的进程。
(5)查找某个进程:根据进程的名字或者其他信息,结合 grep 命令找到目标进程。
【补充】如上图所示,STAT 表示进程状态,具体参数意义如下:
| 参数 | 含义 |
|---|---|
| D | 不可中断 Uninterruptible(usually IO) |
| R | 正在运行,或在队列中的进程 |
| S(大写) | 处于休眠状态 |
| T | 停止或被追踪 |
| Z | 僵尸进程 |
| W | 进入内存交换(从内核2.6开始无效) |
| X | 死掉的进程 |
| < | 高优先级 |
| N | 低优先级 |
| s | 包含子进程 |
| + | 位于前台的进程组 |
top 命令用来动态显示运行中的进程。top 命令能够在运行后,在指定的时间间隔更新显示信息,可以在使用 top命令时加上 -d 来指定显示信息更新的时间间隔。在top命令执行后,可以按下按键得到对显示的结果进行排序:
| 按键 | 含义 |
|---|---|
| M | 根据内存使用量来排序 |
| P | 根据CPU占有率来排序 |
| T | 根据进程运行时间的长短来排序 |
| U | 可以根据后面输入的用户名来筛选进程 |
| K | 可以根据后面输入的PID来杀死进程。 |
| q | 退出 |
| h | 获得帮助 |
![]() | |
| 【备注】top 命令类似于 windows 操作系统上的任务管理器。 |
jobs 命令用于查看当前终端的所有后台进程。该命令可以显示任务号及其对应的进程号。其中,任务号是以普通用户的角度进行的,而进程号则是从系统管理员的角度来看的。一个任务可以对应于一个或者多个进程号。常用选项如下:
| 参数 | 含义 |
|---|---|
| -l | 显示进程号 |
| -p | 仅任务对应的显示进程号 |
| -n | 显示任务状态的变化 |
| -r | 仅输出运行状态(running)的任务 |
| -s | 仅输出停止状态(stoped)的任务 |
![]() |
参考:Linux 基础介绍-基础命令 一文 20.3 小结
参考:Linux 基础介绍-基础命令 一文 20.4 小结
Linux下,需要经常使用进程的前后台调度命令,比如一个需要长时间运行的命令,我们就希望把它放入后台,这样就不会阻塞当前的操作;还有一些服务型的命令进程我们则希望能把它们长期运行于后台。
nohup ./server &。【补充】在默认情况下(非重定向时),会输出一个名叫 nohup.out 的文件到当前目录下,如果当前目录的 nohup.out 文件不可写,输出重定向到 $HOME/nohup.out 文件中。示例 1:
yxm@192:~/myshare/NetworkIO/BIO/mul_process_server$ jobs -l # 列出后台进程,无后台进程
yxm@192:~/myshare/NetworkIO/BIO/mul_process_server$ ./server # 运行程序
Accepting connections ...
^Z # 执行 ctrl + z,暂停并转为后台
[1]+ Stopped ./server
yxm@192:~/myshare/NetworkIO/BIO/mul_process_server$ jobs -l # 列出后台进程,存在后台进程
[1]+ 20743 Stopped ./server
yxm@192:~/myshare/NetworkIO/BIO/mul_process_server$ bg 1 # 将暂停的后台进程恢复运行
[1]+ ./server &
yxm@192:~/myshare/NetworkIO/BIO/mul_process_server$ fg 1 # 将后台进程转为前台
./server
^C # 执行 ctrl + c 结束进程
yxm@192:~/myshare/NetworkIO/BIO/mul_process_server$
示例 2:
yxm@192:~/myshare/NetworkIO/BIO/mul_process_server$ jobs -l # 列出后台进程,无后台进程
yxm@192:~/myshare/NetworkIO/BIO/mul_process_server$ nohup ./server & # 以非挂起的方式运行程序
[1] 21292
yxm@192:~/myshare/NetworkIO/BIO/mul_process_server$ nohup: ignoring input and appending output to 'nohup.out'yxm@192:~/myshare/NetworkIO/BIO/mul_process_server$ jobs -l # 列出后台进程,无后台进程
[1]+ 21292 Running nohup ./server &
参考文章1
参考文章2
(1)getpid 函数
#include
#include pid_t getpid(void);
功能:获取本进程号(PID)
参数:无
返回值:本进程号(注意:该函数总是执行成功,所以返回值不需要进行错误检测)
(2)getppid函数
#include
#include pid_t getppid(void);
功能:获取调用此函数的进程的父进程号(PPID)
参数:无
返回值:调用此函数的进程的父进程号(PPID)
(3)getpgid函数
#include
#include pid_t getpgid(pid_t pid);
功能:获取进程组号(PGID)
参数:pid:进程号
返回值:参数为 0 时返回当前进程组号,否则返回参数指定的进程的进程组号
示例程序:
// test.c
#include
#include
#include // 获取进程号、父进程号、进程组号
int main() {pid_t pid, ppid, pgid;pid = getpid();printf("pid = %d\n", pid);ppid = getppid();printf("ppid = %d\n", ppid);pgid = getpgid(pid);printf("pgid = %d\n", pgid);return 0;
}
yxm@192:~$ gcc test.c -o test
yxm@192:~$ ./test
pid = 27095
ppid = 26906
pgid = 27095
系统允许一个进程创建新进程,新进程即为子进程,子进程还可以创建新的子进程,形成进程树结构模型。
#include
#include pid_t fork(void);
功能:用于从一个已存在的进程中创建一个新进程,新进程称为子进程,原进程称为父进程。
参数:无
返回值:成功:本函数返回值会返回两次,一次是子进程中返回 0,一次是父进程中返回子进程 ID。失败:父进程中返回 -1,API。失败的两个主要原因是:1)当前的进程数已经达到了系统规定的上限,这时 errno 的值被设置为 EAGAIN。2)系统内存不足,这时 errno 的值被设置为 ENOMEM。
示例代码:
#include
#include
#include int main() {fork();printf("id ==== %d\n", getpid()); // 获取进程号 return 0;
}
运行结果如下:
yxm@192:~$ gcc test.c -o test
yxm@192:~$ ./test
id ==== 27332 # 父进程的进程号
id ==== 27333 # 子进程的进程号
从运行结果,我们可以看出,fork() 之后的打印函数打印了两次,而且打印了两个进程号,这说明,fork() 之后确实创建了一个新的进程,新进程为子进程,原来的进程为父进程。
上一小节中,我们使用 fork() 函数得到的子进程实际上是父进程的一个复制品,它从父进程处复制了整个进程的虚拟地址空间:包括进程上下文(进程执行活动全过程的静态描述)、进程堆栈、打开的文件描述符、信号控制设定、进程优先级、进程组号等。
子进程所独有的只有它的进程号,计时器等(只有小量信息)。因此,使用 fork() 函数的代价是很大的。

fork() 函数后,系统先给新的进程分配资源,例如存储数据和代码的空间。然后把原来的进程的所有值都复制到新的新进程中,只有少数值与原来的进程的值不同。相当于克隆了一个自己。fork() 使用是通过写时拷贝 (copy- on-write) 技术来实现克隆自己的。写时拷贝是一种可以推迟甚至避免拷贝数据的技术。内核一开始并不复制整个进程的地址空间,而是让父子进程共享同一个地址空间。只有在需要写入的时候才会复制地址空间,从而使各个进程拥有各自的地址空间。也就是说,资源的复制是在需要写入的时候才会进行,在此之前,只有以只读方式共享——写时拷贝,读时共享。fork() 之后父子进程共享文件, fork() 产生的子进程与父进程相同的文件文件描述符指向相同的文件表,引用计数增加,共享文件文件偏移指针。

子进程是父进程的一个复制品,可以简单认为父子进程的代码一样的。那如果这样的话,父进程做了什么事情,子进程也做什么事情(如上面的例子),是不是不能满足实现多任务的要求(多任务一般是父进程做一件事,子进程做另外一件事,从而实现并发)。
实际上可以通过 fork() 的返回值区别父子进程:fork() 函数被调用一次,但返回两次。两次返回的区别是:子进程的返回值是 0,而父进程的返回值则是新子进程的进程 ID。
测试程序如下:
#include
#include
#include int main() {pid_t pid;pid = fork();if (pid < 0) { // 没有创建成功 perror("fork");return 0;}if (0 == pid) { // 子进程 while (1) {printf("I am son\n");sleep(1);}} else if (pid > 0) { // 父进程 while (1) {printf("I am father\n");sleep(1);}}return 0;
}
yxm@192:~$ gcc test.c -o test
yxm@192:~$ ./test
I am father
I am son
I am father
I am son
...
^C
yxm@192:~$
运行结果如下:通过运行结果,可以看到,父子进程各做一件事(各自打印一句话)。这里,我们只是看到只有一份代码,实际上,fork() 以后,有两个地址空间在独立运行着,有点类似于有两个独立的程序(父子进程)在运行。
fork() 这个函数后才开始执行代码。
fork() 函数的返回值不一样而调用不同的代码段。fork() 这个函数后才开始执行代码,所以在连续创建子进程或者循环创建子循环时候需要小心,否则创建的子进程数量可能远远超过预想的子进程数量,如下所示#include
#include
// 想要连续创建两个进程
int main() {pid_t pid;// 连续创建两个子进程fork();fork();return 0;
}
上例中,想要连续创建两个子进程,但实际上创建了三个子进程,因为第一个 fork() 之后创建了一个子进程,而该子进程从第一个 fork() 函数后才开始执行代码,所以会执行第二个 fork() 创建了一个孙进程,再加上父进程执行第二个 fork() 函数又会创建新的子进程,一共创建了三个进程既然可以创建进程,那如何结束一个进程呢?
main() 以外的函数中终止,要实现这一点可以使用 exit() 函数。#include
void exit(int status);#include
void _exit(int status);
功能:结束调用此函数的进程。
参数:status:父进程回收子进程资源的时获取进程退出时的一个状态信息(在父子进程中,如果子进程退出了,_exit就能得到子进程退出的状态)。
返回值:无
_exit() 和 exit() 函数功能和用法是一样的,但还是有两点区别:
exit() 属于标准库函数(标准 c 库中的函数),_exit() 属于系统调用函数(linux 系统中的函数)。由于 exit() 底层会调用 _exit() 函数,其在调用_exit() 函数之前,会做一些安全处理,所以使用 exit() 相对于直接使用 _exit() 更加安全。系统调用请参考:Linux 常用 API 函数 一文 1.1 小结
exit()示例:
#include
#include int main() {printf("hello\n");printf("world");exit(0); // 等价于return 0;return 0;
}
yxm@192:~$ gcc test.c -o test
yxm@192:~$ ./test
hello
worldyxm@192:~$
_exit()示例:
#include
#include int main() {printf("hello\n");printf("world");_exit(0);return 0;
}
worldyxm@192:~$ gcc test.c -o test
yxm@192:~$ ./test
hello
yxm@192:~$
exit() 与 _exit() 的运行结果如下:
printf() 中有 \n 换行,带上换行之后,printf() 内部会自动实现刷新缓冲区的功能,所以 exit()示例与 _exit()示例中的 ‘hello’ 都被打印出来了;printf() 中没有 \n 换行,且 _exit() 的内部没有刷新缓冲区,所以 _exit()示例中的 ‘world’ 遗留在缓冲区,在还没来得及输出到标准输出文件(stdout)的情况系,程序就已经结束, ‘world’ 没有来得及打印。常用调用
exit(1) 表示进程正常退出,返回 1exit(0) 表示进程非正常退出,返回 0.
在每个进程退出的时候,内核释放该进程所有的资源、包括打开的文件、占用的内存等。但是仍然为其保留一定的信息,这些信息主要是进程控制块 PCB 的信息(包括进程号、退出状态、运行时间等)。
父进程可以通过调用 wait() 或 waitpid() 等到它的子进程退出状态同时彻底清除掉这个进程。所以子进程运行结束或者进程退出时,父进程有义务回收子进程的资源。
wait() 和 waitpid() 函数的功能一样,区别在于,wait() 函数会阻塞,waitpid() 可以设置不阻塞,waitpid() 还可以指定等待哪个子进程结束,下文会详细介绍。
【注意】一次 wait() 或 waitpid() 调用只能清理一个子进程,清理多个子进程要使用循环。
(1)wait函数
#include
#include pid_t wait(int *status);
功能:等待任意一个子进程结束,如果任意一个子进程结束了,此函数会回收该子进程的资源。
参数:status:进程退出时的状态信息,本参数是传出参数。
返回值:成功:已经结束子进程的进程号失败:返回 -1, 并设置errno,一般失败的原因:1、没有任何子进程;2、所有的子进程都已结束;3、函数调用函数失败。
wait() 就会把子进程退出时的状态取出并存入其中,这是一个整数值(int),指出了子进程是正常退出还是被非正常结束的。这个退出信息在一个 int 中包含了多个字段,直接使用这个值是没有意义的,我们需要用宏定义取出其中的每个字段。退出信息相关宏函数可分为如下三组: 示例:
#include
#include
#include
#include
#include
#include int main() {int status = 0;int i = 0;int ret = -1;pid_t pid = -1;// 创建子进程pid = fork();if (pid < 0) { // 没有创建成功 perror("fork");return -1;}if (0 == pid) {// 子进程 for (i =0; i < 5; i++) {printf("child process do thing %d\n", i + 1);sleep(1);}exit(10); //子进程终止}// 父进程执行printf("父进程等待子进程退出,回收其资源\n");ret = wait(&status); // 父进程在此处会阻塞,等待子进程退出,返回值为exit函数的参数if (-1 == ret) {perror("wait");return -1;}printf("父进程回收了子进程资源\n");if (WIFEXITED(status)) {//属于正常退出printf("子进程退出状态码:%d\n", WEXITSTATUS(status));}else if (WIFSIGNALED(status)) {//属于异常终止退出printf("子进程被信号%d杀死了...\n", WTERMSIG(status));}else if (WIFSTOPPED (status)) {//属于进程暂停printf("子进程被信号%d暂停...\n", WSTOPSIG(status ));}return 0;
}
yxm@192:~$ gcc test.c -o test
yxm@192:~$ ./test
父进程等待子进程退出,回收其资源
child process do thing 1
child process do thing 2
child process do thing 3
child process do thing 4
child process do thing 5
父进程回收了子进程资源
子进程退出状态码:10
(2)waitpid函数
#include
#include pid_t waitpid(pid_t pid, int *status, int options);
功能:等待指定进程号终止,如果子进程终止了,此函数会回收子进程的资源,可以设置是否阻塞参数:pid:参数 pid 的值有以下几种类型:pid > 0 某个子进程的进程号,相当于等待并回收指定子进程。pid = 0 等待并回收同一个进程组中的任何子进程,如果子进程已加入了别的进程组,waitpid不会等待它。pid = -1 等待并回收任一子进程,此时 waitpid 和 wait 作用一样(最常用)。pid < -1 等待并回收指定进程组中的任何子进程,这个进程组的 ID 等于 pid 的绝对值。status:进程退出时的状态信息。和 wait() 用法一样。options:options提供了一些额外的选项来控制 waitpid()。0:同 wait() 一样,阻塞父进程,等待子进程退出。WNOHANG:非阻塞。WUNTRACED:如果子进程暂停了则此函数马上返回,并且不予以理会子进程的结束状态。(由于涉及到一些跟踪调试方面的知识,加之极少用到)返回值:waitpid() 的返回值比 wait() 稍微复杂一些,一共有 3 种情况:1) 当正常返回的时候,waitpid() 返回收集到的已经回收子进程的进程号,则返回 > 0;2) 如果设置了选项 WNOHANG,而调用中 waitpid() 发现没有已退出的子进程可等待,则返回 0;3) 如果调用中出错,则返回-1,这时 errno 会被设置成相应的值以指示错误所在,如:当 pid 所对应的子进程不存在,或此进程存在,但不是调用进程的子进程, waitpid() 就会出错返回,这时 errno 被设置为 ECHILD;
示例:
#include
#include
#include
#include
#include int main() {// 有一个父进程,创建5个子进程(兄弟)pid_t pid;// 创建5个子进程for(int i = 0; i < 5; i++) {pid = fork();if(pid == 0) {// 子进程还会创建新的子进程,所以需要判断语句以保证只会创建 5 个子进程break;}}if(pid > 0) {// 父进程while(1) {printf("parent, pid = %d\n", getpid());sleep(1);int st;// int ret = waitpid(-1, &st, 0);int ret = waitpid(-1, &st, WNOHANG);if(ret == -1) {break;} else if(ret == 0) {// 说明还有子进程存在continue;} else if(ret > 0) {if(WIFEXITED(st)) {// 是不是正常退出printf("退出的状态码:%d\n", WEXITSTATUS(st));}if(WIFSIGNALED(st)) {// 是不是异常终止printf("被哪个信号干掉了:%d\n", WTERMSIG(st));}printf("child die, pid = %d\n", ret);} }} else if (pid == 0) {// 子进程while(1) {printf("child, pid = %d\n",getpid()); sleep(1); }exit(0);}return 0;
}
在 windows 平台下,我们可以通过双击运行可执行程序,让这个可执行程序成为一个进程;而在 linux 平台,我们可以通过 ./ 让一个可执行程序成为一个进程。
但是,如果我们本来就运行着一个程序(进程),如何在这个进程内部启动一个外部程序,由内核将这个外部程序读入内存,使其执行起来成为一个进程呢?这里就可以通过进程替换相关的 API 来实现!
Linux 下我们可以通过库函数实现进程替换—— exec 函数族。
进程替换 API
exec 函数族是一簇函数,Linux 中并不存在 exec() 函数,exec 指的是一组函数,一共有 6 个,其中使用最多的是 execl() 和 execlp():
#include
extern char **environ;int execl(const char *path, const char *arg, .../* (char *) NULL */);
int execlp(const char *file, const char *arg, ... /* (char *) NULL */);
int execle(const char *path, const char *arg, .../*, (char *) NULL, char * const envp[] */); // ...表示可变参数
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);int execve(const char *filename, char *const argv[], char *const envp[]);【注意】exec函数族的参数都是 const char *,不是 std::string 类型
(1)exec 函数族的作用是根据指定的文件名或目录名找到可执行文件,并用它来取代调用进程的内容,换句话说,就是在调用进程内部执行一个可执行文件。
(2)exec 函数族与一般的函数不同,exec 函数族中的函数执行成功后不会返回,而且,exec 函数族后面的代码执行不到。只有调用失败才会返回 -1,失败后从原程序的调用点接着往下执行。
(3)exec 函数族使用说明:exec 函数族的 6 个函数看起来似乎很复杂,但实际上无论是作用还是用法都非常相似,只有很微小的差别。
| 参数类型 | 说明 |
|---|---|
| l(list) | 参数地址列表,以空指针结尾 |
| v(vector) | 存有各参数地址的指针数组的地址 |
| p(path) | 按 PATH 环境变量指定的目录搜索可执行文件 |
| e(environment) | 存有环境变量字符串地址的指针数组的地址 |
| 函数名 | 参数传递形式 | 路径 | 是否导入环境变量 |
|---|---|---|---|
| execl | 列表 | 需要可执行程序路径 | 不导入 使用当前环境变量 |
| execlp | 列表 | 默认在环境变量中找 | 不导入 使用当前环境变量 |
| execle | 列表 | 需要可执行程序路径 | 导入 使用导入的环境变量 |
| execv | 数组 | 需要可执行程序路径 | 不导入 使用当前环境变量 |
| execvp | 数组 | 默认在环境变量中找 | 不导入 使用当前环境变量 |
| execve | 数组 | 需要可执行程序路径 | 导入 使用导入的环境变量 |
(4)事实上,只有 execve 是真正意义上的系统调用,其它都是在此基础上经过包装的库函数,即其他五个函数最终都调用 execve。
进程替换不会创建新的进程,进程 PCB未发生改变,进程实体(数据代码内容)被替换:进程调用 exec 函数时,该进程完全由新程序替换,而新程序则从其 main 函数开始执行。因为调用 exec 并不创建新进程,所以前后的进程 ID (当然还有父进程号、进程组号、当前工作目录……)并未改变。exec 只是用另一个新程序替换了当前进程的正文、数据、堆和栈段(进程替换)。

// main程序,main程序中再进行进程替换成ps -f程序
#include
#include
#include // ls -l /home
int main ()
{printf("hello itcast\n");// arg0 arg1 arg2 .... argn// arg0一般是可执行文件名, argn必须是NULL//execlp("ls", "ls", "-l", "/home", NULL);// 第一个参数是可执行文件的相对路径或者绝对路径// 第二个参数是可执行文件的名字// 中间的参数是可执行文件的参数// 最后一个参数必须是NULL//execl("/bin/ls", "ls", "-l", "/home", NULL);// 第一个参数是可执行文件的名字// 第二个参数是指针数组,最后一定以NULL结束// char *argv[] = {"ls", "ls", "-l", "/home", NULL};// execvp("ls", argv);// 最后一个参数是环境变量指针数组,最后一定以NULL结束char *envp[] = {"ADDR=BEIJING", NULL};execle("ls", "ls", "-l", "/home", NULL, envp);printf("hello world\n"); // 注意:如果进程替换执行成功,本行不会被执行return 0;
}

当然我们也可以用 fork 创建子进程后,主进程继续执行原有程序,子进程调用一种 exec 函数以执行另一个程序。
#include
#include
#include
#includechar *argv[8];
int argc = 0;void do_parse(char *buf) {int i;int status = 0;for(argc=i=0; buf[i]; i++) {if(!isspace(buf[i]) && status == 0) {argv[argc++] = buf+i;status = 1;} else if (isspace(buf[i])) {status = 0;buf[i] = 0;}}argv[argc] = NULL;
}void do_execute(void) {pid_t pid = fork();switch(pid) {case -1;perror("fork");exit(EXIT_FAILURE);break;case 0;execvp(argv[0], argv);perror("execvp");exit(EXIT_FAILURE);default:{int st;while(wait(&st) != pid);}}
}int main() {char buf[1024] = {};while(1) {printf("myshell>");scanf("%[^\n]%*c", buf);do_parse(buf);do_execute();}
}
替换的过程
除了 exec 函数族之外,还有另外一种方法可以在一个进程内部启动一个外部程序—— system 系统调用。
先来看一下 system() 函数的简单介绍:
//头文件
#include //函数定义
int system(const char * string);
参数:被请求变量名称的 C 字符串。
返回值:如果发生错误,则返回值为 -1,否则返回命令的状态。
system() 会调用 fork() 产生子进程,由子进程来调用 /bin/sh-cstring 来执行参数 string 所代表的命令。此命令执行完后随即返回原调用的进程。system() 期间 SIGCHLD 信号会被暂时搁置,SIGINT 和 SIGQUIT 信号则会被忽略。fork() 失败返回 -1:出现错误waitpid() 函数获得的子进程的返回状态。为了更好的理解 system () 函数返回值,做好出错处理,需要了解其执行过程,实际上 system () 函数执行了三步操作:
看一下 system () 函数的源码
int system(const char * cmdstring) {pid_t pid;int status;if(cmdstring == NULL) {return (1); //如果cmdstring为空,返回非零值,一般为1}if((pid = fork())<0) {status = -1; //fork失败,返回-1} else if(pid == 0) {execl("/bin/sh", "sh", "-c", cmdstring, (char *)0);_exit(127); // exec执行失败返回127,注意exec只在失败时才返回现在的进程,成功的话现在的进程就不存在啦~~} else {//父进程while(waitpid(pid, &status, 0) < 0) {if(errno != EINTR) {status = -1; //如果waitpid被信号中断,则返回-1break;}}}return status; //如果waitpid成功,则返回子进程的返回状态
}
exec 函数族与 system() 的区别
system() 后则相当于 fork() 出一个子进程,并等待此子进程执行完毕。所以 system() 只能在一个进程内部启动一个外部程序,但是并不能真正替换原来的进程。实际开发中,建议使用 system() ,因为 system() 会创建子进程,更加安全(当然效率比 exec 低一些)。参考文章:
参考文档1
参考文档2
什么是孤儿进程?
父进程运行结束,但子进程还在运行(未运行结束),这样的子进程就称为孤儿进程(Orphan Process)
每当出现一个孤儿进程的时候,内核就把孤儿进程的父进程设置为 init ,而 init 进程会循环地 wait() 它的已经退出的子进程。这样,当一个孤儿进程凄凉地结束了其生命周期的时候,init 进程就会代表党和政府出面处理它的一切善后工作。 因此孤儿进程并不会有什么危害。
总之:孤儿进程就是父进程退出了,但子进程还在执行。
示例:
#include
#include
#include
#include int main() {pid_t pid = -1;// 创建子进程pid = fork();if (pid < 0) { // 没有创建成功 perror("fork");return 1;}// 父进程if (pid > 0) {printf("父进程休息3秒后退出。。。\n");printf("父进程: pid:%d\n", getpid());sleep(1);printf("父进程等太累了,现退出了。。。\n");exit(0);}while (1) {printf("子进程不停的工作,子进程:pid:%d,父进程:ppid:%d\n", getpid(), getppid());sleep(1);}return 0;
}
运行结果:
yxm@192:~$ gcc test.c -o test
yxm@192:~$ ./test
父进程休息3秒后退出。。。
父进程: pid:32075
子进程不停的工作,子进程:pid:32076,父进程:ppid:32075
父进程等太累了,现退出了。。。
子进程不停的工作,子进程:pid:32076,父进程:ppid:32075
yxm@192:~$ 子进程不停的工作,子进程:pid:32076,父进程:ppid:1 # 终端可以输入,同时有数据在输出
子进程不停的工作,子进程:pid:32076,父进程:ppid:1
子进程不停的工作,子进程:pid:32076,父进程:ppid:1
每个进程结束之后, 都会释放自己地址空间中的用户区数据,内核区的 PCB 没有办法自己释放掉(子进程残留资源(PCB)存放于内核中),需要父进程去释放,进程终止时,父进程尚未回收,子进程残留资源(PCB)存放于内核中,变成僵尸(Zombie)进程。
僵尸进程不能被 kill -9 杀死,这样就会导致一个问题,如果父进程不调用 wait() 或 waitpid() 的话,那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵尸进程,将因为没有可用的进程号而导致系统不能产生新的进程,此即为僵尸进程的危害,应当避免。
总之:僵尸进程就是子进程结束了,但父进程没有回收其资源。
#include
#include
#include
#include int main() {int i = 0;pid_t pid = -1;// 创建子进程pid = fork();if (-1 == pid) { // 没有创建成功 perror("fork");return 1;}// 子进程if (0 == pid) {for (int i = 0; i < 5; i++) {printf("子进程做事%d\n", i);sleep(1);}printf("子进程想不开,结束了自己。。。。\n");exit(0);} else if (pid > 0) {while(1) {printf("父进程休眠了, pid : %d, ppid : %d\n", getpid(), getppid());sleep(1);}}return 0;
}
yxm@192:~$ gcc test.c -o test
yxm@192:~$ ./test
父进程休眠了, pid : 33087, ppid : 30344
子进程做事0
父进程休眠了, pid : 33087, ppid : 30344
子进程做事1
子进程做事2
父进程休眠了, pid : 33087, ppid : 30344
父进程休眠了, pid : 33087, ppid : 30344
子进程做事3
子进程做事4
父进程休眠了, pid : 33087, ppid : 30344
父进程休眠了, pid : 33087, ppid : 30344
子进程想不开,结束了自己。。。。
父进程休眠了, pid : 33087, ppid : 30344
^C
yxm@192:~$ ps -aux
...
...
yxm 33087 0.0 0.0 4516 756 pts/0 S+ 00:10 0:00 ./test
yxm 33088 0.0 0.0 0 0 pts/0 Z+ 00:10 0:00 [test] #僵尸进程
yxm 33125 0.0 0.0 7476 832 ? S 00:10 0:00 sleep 180
yxm 33180 0.0 0.1 37860 3420 pts/1 R+ 00:10 0:00 ps -aux
方式一:僵尸进程的产生是因为父进程没有 wait() 子进程。所以如果我们自己写程序的话一定要,最好在父进程中通过 wait() 和 waitpid() 来避免僵尸进程的产生。
方式二:当系统中出现了僵尸进程时,我们是无法通过 kill 命令把它清除掉的。但是我们可以杀死它的父进程,让它变成孤儿进程,并进一步被系统中管理孤儿进程的进程收养并清理。具体步骤如下:
ps -e -o stat,ppid,pid,cmd | egrep '^[Zz]'
参数说明: kill -9 父进程 pid。kill 之后,僵尸进程将被 init 进程收养并清理【补充】现在大多数 linux 系统,会将僵尸进程标识为 defunct,所以也可以通过如下命令来获取僵尸进程信息:
ps -ef | grep "defunct"
孤儿进程与僵尸进程是两种特殊的进程,一种是父进程先退出,子进程变成孤儿,这种进程没有危害;一种是子进程先退出,父进程没有回收资源导致子进程变成僵尸,会占用系统资源。他们都发生过在父子进程之间。