目录
一、进程创建
1. fork函数
2. fork函数返回值
3. 写时拷贝
4. fork常规用法
5. fork调用失败原因
6. 如何创建多个子进程?
二、进程终止
1. 进程退出场景
2. 进程退出码
3. errno
4. 进程异常退出
5. 进程常见退出方法
5.1 return退出
5.2 exit退出
5.3 _exit退出
小结
三、进程等待
1. 进程等待的必要性
2. 进程等待的方法
2.1 wait 方法
2.2 waitpid
3. 获取子进程status
4. options参数
5. wait / waitpid原理
四、进程程序替换
1. 替换原理
2. 7个替换函数
execl
execlp
execv
execvp
make形成多个可执行程序
execle
新增环境变量
覆盖环境变量
小结:
一、进程创建
1. fork函数
#include <unistd.h>
pid_t fork(void);
返回值:自进程中返回0,父进程返回子进程id,出错返回-1
进程调用fork,当控制转移到内核中的fork代码后,内核工作:
- 分配新的内存块和内核数据结构给子进程
- 将父进程部分数据结构内容(PCB、进程地址空间、页表)拷贝至子进程
- 添加子进程到系统进程列表当中(将PCB链入系统运行队列中)
- fork返回,开始调度器调度
fork简单样例:
31685进程创建子进程31686,fork函数对父进程返回子进程pid,对子进程返回0表示创建成功,如果返回-1表示创建失败
2. fork函数返回值
- 对子进程返回0表示创建成功,-1表示失败
- 对父进程返回子进程的pid
fork函数为什么要给子进程返回0,给父进程返回子进程的PID?
一个父进程可以创建多个子进程,而一个子进程只能有一个父进程。因此,对于子进程来说,父进程是不需要被标识的;而对于父进程来说,子进程是需要被标识的,因为父进程创建子进程的目的是让其执行任务的,父进程只有知道了子进程的PID才能很好的对该子进程指派任务。
为什么fork函数有两个返回值?
父进程调用fork函数后,为了创建子进程,fork函数内部将会进行一系列操作,包括创建子进程的进程控制块、创建子进程的进程地址空间、创建子进程对应的页表等等。子进程创建完毕后,操作系统还需要将子进程的进程控制块添加到系统进程列表当中,此时子进程便创建完毕了。
也就是说,在fork函数内部执行return语句之前,子进程就已经创建完毕了,那么之后的return语句父子共享,这就是fork函数有两个返回值的原因
fork的具体细节我们在进程概念处已经讲过,此处不再赘述
3. 写时拷贝
当一个进程要创建子进程时,在进程地址空间内原只读数据权限不变,而可写数据权限更变为只读。显然,当子进程拷贝父进程的进程地址空间后,两者都为只读权限,所以父子进程任何一方要修改数据时,不会发生异常处理,而是触发写时拷贝,开辟新的空间来存放新的数据,再修改该数据的页表映射即可
4. fork常规用法
- 一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。
- 一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数
5. fork调用失败原因
- 系统中有太多的进程
- 实际用户的进程数超过了限制
6. 如何创建多个子进程?
循环一定次数即可,这里主函数处父进程会提前结束,所以让父进程睡眠1000s等待子进程
Z+、defunct表示僵尸进程,因为父进程没有获取子进程信息,所以子进程一直处于僵尸状态
二、进程终止
1. 进程退出场景
- 代码运行完毕,结果正确
- 代码运行完毕,结果不正确
- 代码异常终止
正不正确将统一采用进程的退出码来判定,即main函数的return值
当进程异常退出后,进程的退出码就没有意义了,我们要关心的就是为什么异常,以及发生了什么异常
2. 进程退出码
我们写C、C++语言时,在主函数内总是return 0;这是为什么呢?
这里的0指的是进程的退出码,表征进程的运行结果是否正确。本质上表示该进程运行完成时是否正确的结果,如果不是,可以用不同的数字表示出错原因。
main函数是间接性被操作系统所调用的,那么当main函数调用结束后就应该给操作系统返回相应的退出信息,而这个所谓的退出信息就是以退出码的形式作为main函数的返回值返回,我们一般以0表示代码成功执行完毕,以非0表示代码执行过程中出现错误,这就是为什么我们都在main函数的最后返回0的原因。
那么这个0被谁拿到了呢?
被父进程即bash拿到了,可以用下面的指令查看命令行中最近一个进程退出时的退出码
所以如果连续两次 echo $? 那么第二次的echo将会输出0,因为最近一个进程就是上一个echo,该进程是正常退出的,所以退出码是0
echo $?
进程中,谁会关心“我”运行的情况呢?
一般而言,是该进程的父进程要关心!因为父进程创建子进程就是要让子进程去干一些事情,所以子进程运行结束,父进程是需要知道运行的结果如何。但是父进程不会关心子进程为什么运行成功了,而是会关心他为什么运行失败或异常!
所以子进程可以return的不同的数字,来表明运行结果——退出码
这些退出码我们不了解含义,所以有strerror函数可以返回这些退出码的退出码描述
以2号退出码为例,我们ls一个不存在的文件
ls myfile.txt
结果显示No such file or directory,这正是2号退出码的解释,也就是说ls这个进程运行出错了,想bash返回了2号退出码
同样的,我们也可以编写我们自己的错误解释,一个指针数组即可完成
3. errno
C语言中,提供了一个全局变量errno — number of lasr error,即最近一次的错误码,这是因为C语言有许多库函数,如果出错了那就需要出错原因
示例:申请大概4G空间
4. 进程异常退出
代码错误导致进程运行时异常退出
进程出现异常,本质时我们的进程收到了对应的信号。当进程异常退出时,操作系统会向该进程发送错误信号
例如,我们的程序中有分母为0的错误,对应信号为8号SIGFPE,野指针错误对应11号SIGSEGV
向进程发生信号导致进程异常退出
例如,一个正常运行的进程,我们主动向该进程发信号,从而使该进程异常退出
5. 进程常见退出方法
正常终止(可以通过 echo $? 查看进程退出码):
1. 从main返回
2. 调用exit
3. _exit
5.1 return退出
在main函数内return退出进程是我们最常用的方法
5.2 exit退出
可以看下面样例的输出
void fun()
{
printf("hello world!\n");
printf("hello world!\n");
printf("hello world!\n");
exit(13);
}
int main()
{
printf("hello Linux!\n");
fun();
//exit(12);
return 12;
}
输出
echo $?
13
exit 函数在退出进程前会做一系列工作:
- 执行用户通过atexit或on_exit定义的清理函数
- 关闭所有打开的刘,所有的缓存数据均被写入
- 调用_exit函数终止进程
printf 一定是先把数据写入缓冲区中,合适的时候(\n等)再进行刷新
例如,exit 终止进程前会换新缓冲区
可以看到即使没有\n,最终也刷新了缓冲区数据打印在屏幕上
5.3 _exit退出
同样的,_exit 也可以在代码中任意位置退出进程,但是它并不会在退出进程前做任何收尾工作
例如,使用_exit 函数终止进程,缓冲区不会刷新输出
侧面证明了缓冲区并不在操作系统的内核部分,因为如果在内核,那么 _exit 也应该刷新缓冲区,操作系统是不会容忍任何浪费空间效率的行为
小结
- exit 、_exit 在任意地方被调用都表示调用进程直接退出
- return 是当前函数返回,只有在main函数内部return表示进程退出,因为主函数结束后,会将main函数的返回值作为 exit 函数的参数调用 eixt 函数
- _exit 是系统调用,exit是库函数
- 使用exit函数退出进程前,exit函数会执行用户定义的清理函数、冲刷缓冲,关闭流等操作,然后再调用_exit 系统调用 终止进程,而_exit 会直接终止进程,不会做任何收尾工作
三、进程等待
是什么:通过系统调用 wait / waitpid,来进行对子进程状态检测与回收功能
为什么:因为僵尸进程无法被杀死,需要通过进程等待来杀掉它,进而解决内存泄露问题
怎么办:父进程通过wait、waitpid进程僵尸子进程的回收
1. 进程等待的必要性
- 子进程退出,父进程如果不管不顾,就可能造成 ‘僵尸进程’ 的问题(Z状态),进而造成内存泄漏。
- 另外,进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的 kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。
- 最后,父进程派给子进程的任务完成的如何,父进程需要知道它布置给子进程的任务,子进程完成的如何。如,子进程运行完成,结果正确还是不正确,或者是否异常退出。
- 父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息
2. 进程等待的方法
2.1 wait 方法
wait 要包含两个头文件,形参我们暂时传NULL
举例,父进程wait 等待僵尸子进程
当父进程wait后,子进程由僵尸进程转为真正的死亡进程。但是如果父进程创建了多个子进程,那么wait返回的是哪一个子进程呢?又该如何等待呢?
当gcc版本低时 ( for循环内不能定义变量),可以在编译时加上 -std=c99
举例,多个子进程的wait,循环wait即可
要实现多个子进程的wait,直接循环N次,判断wait的返回值是否大于0即可
wait阻塞
如果在上面的样例中,我们使子进程死循环,一直不推出,那么父进程的wait还有效吗?
无效!父进程的wait将会处于阻塞状态,父进程一直运行,一直等待子进程的退出
如果子进程不退出,父进程默认在wait时,也就不返回,默认叫做阻塞状态
2.2 waitpid
wait所能提供的功能是waitpid的子集
pid_ t waitpid ( pid_t pid, int *status, int options);
返回值:
- 当正常返回的时候waitpid返回收集到的子进程的进程ID;
- 如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
- 如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;
参数:
pid:
- Pid = -1,等待任一个子进程,与wait等效。
- Pid > 0,等待其进程ID与pid相等的子进程。
status:
- WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
- WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
options:
WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID
与之前举例效果相同
3. 获取子进程status
wait以及waitpid都涉及到了一个整形指针status参数,它的作用是什么呢?
该参数是一个输出型参数,是父进程用来获取子进程退出信息的方式,父进程可以不获取子进程信息,直接将status参数处传NULL即可,这表示父进程不关心子进程的退出状态信息,但是父进程不能没有获取子进程信息的方式!
通过传入指针的形式,在函数内部可以修改该整形变量,从而达到传递信息的功能
所以,我们会在父进程内定义一个整型变量status,将它的地址传入wait或waitpid函数中
status整形变量是如何存储信息的呢?
我们不能简单的将status视为普通的整形来看待,因为status的不同bit位所代表的信息不同。
status是int型变量占4字节,共有32bit位,高位的16bit位不使用,而从低位开始的7bit位,它们存储该进程退出的异常码(可以使用kill -l查看),因为异常码范围是【1,64】,所以只需要7个bit位。
第 8 bit 位为 core dump 标志,我们在后面的学习中再谈。
【9, 16】bit位存储进程的退出状态码
我们通过一系列位操作,就可以根据status得到进程的退出码和退出信号。
exitSignal = status & 0x7F; //退出信号
exitCode = (status >> 8) & 0xFF; //退出码
0xFF:全1
0x7F:0111 1111
如果将子进程死循环,那么父进程的 waitpid 将会一直等待子进程的退出,当我们
kill -9 子进程pid
子进程立即死亡,父进程打印出的exit sig即为9
系统提供了两个宏简化status内两个信息获取
status:
- WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
- WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
4. options参数
阻塞等待 —— 0
如果在wait、waitpid函数中,指定pid子进程如果没有退出,例如还在R状态,那么父进程只能一直等,并从R状态转为S状态,并脱离CPU上的运行队列,进入子进程PCB中的进程等待队列(父进程处于等待软件就绪),直至子进程退出转为Z状态,父进程获取退出信息,被唤醒,再次修改进程状态为R,进入运行队列
非阻塞轮询
使用宏WNOHANG
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <stdlib.h>
4 #include <sys/types.h>
5 #include <sys/wait.h>
6
7 //#define N 10
8 //
9 //void runChild()
10 //{
11 // int cnt = 3;
12 // while (cnt--)
13 // {
14 // printf("i am child, pid: %d, ppid: %d\n", getpid(), getppid());
15 // sleep(1);
16 // }
17 //
18 //}
19 //
20 int main()
21 {
22 // for (int i = 0; i < N; i++)
23 // {
24 // pid_t id = fork();
25 // if (id == 0)
26 // {
27 // runChild();
28 // exit(i);
29 // }
30 // printf("create child process: %d success\n", id);
31 // }
32 //
33 // sleep(5);
34 //
35 // for (int i = 0; i < N; i++)
36 // {
37 // int status = 0;
38 // pid_t id = waitpid(-1, &status, 0);
39 // if (id > 0)
40 // {
41 // printf("wait %d success, exitCode: %d\n", id, WEXITSTATUS(status));
42 // }
43 // }
44 //
45 // sleep(3);
46 //
47
48 pid_t id = fork();
49
50 if (id < 0)
51 {
52 perror("fork");
53 return 1;
54 }
55 else if (id == 0)
56 {
57 //printf("子进程\n");
58 int cnt = 3;
59 while (cnt)
60 {
61 printf("i am child, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);
62 sleep(1);
63 --cnt;
64 }
65 exit(1);
66 }
67 else
68 {
69 //int cnt = 5;
70 printf("父进程\n");
71 //while (cnt)
72 //{
73 // printf("i am parent, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);
74 // sleep(1);
75 // cnt--;
76 //}
77
78 //pid_t ret = wait(NULL);
79
80 //轮询
81 while (1)
82 {
83 int status = 0;
84 //pid_t ret = waitpid(id, &status, 0);
85 pid_t ret = waitpid(id, &status, WNOHANG); //非阻塞
86 if (ret > 0)
87 {
88 //printf("wait success, ret: %d, status: %d, exitsig: %d, exitcode: %d\n", ret, status, status&0x7F, (status >> 8) & 0xFF );
89 if (WIFEXITED(status))
90 {
91 printf("进程是正常跑完的,退出码: %d\n", WEXITSTATUS(status));
92 }
93 else
94 {
95 printf("进程出异常了\n");
96 }
97 break;
98 }
99 else if (ret < 0)
100 {
101 printf("wait failed!\n");
102 break;
103 }
104 else
105 {
106 //ret == 0
107 printf("子进程还没有退出,我再等等...\n");
108 sleep(1);
109 }
110 }
111 sleep(3);
112 }
113
114
115 return 0;
116 }
在父进程空闲时加入任务,父进程最先开始创建多进程,父进程也是最后退出进程
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#define TASK_NUM 10
typedef void(*task_t)();
task_t tasks[TASK_NUM];
void task1()
{
printf("这是一个执行打印日志的任务, pid: %d\n", getpid());
}
void task2()
{
printf("这是一个中检测网络健康状态的任务, pid: %d\n", getpid());
}
void task3()
{
printf("这是一个进行绘制图形界面的任务, pid: %d\n", getpid());
}
//在InitTask前声明一下Add函数
int AddTask(task_t t);
void InitTask()
{
for (int i = 0; i < TASK_NUM; i++) tasks[i] = NULL;
AddTask(task1);
AddTask(task2);
AddTask(task3);
}
int AddTask(task_t t)
{
int pos = 0;
for (; pos < TASK_NUM; pos++)
{
if (!tasks[pos])
break;
}
if (pos == TASK_NUM)
return -1;
tasks[pos] = t;
return 0;
}
void DelTask()
{}
void Cheack()
{}
void UpdateTask()
{}
//执行任务
void ExecuteTask()
void ExecuteTask()
{
for (int i = 0; i < TASK_NUM; i++)
{
if (!tasks[i]) continue;
tasks[i]();
}
}
int main()
{
pid_t id = fork();
if (id < 0)
{
perror("fork");
return 1;
}
else if (id == 0)
{
//printf("子进程\n");
int cnt = 3;
while (cnt)
{
printf("i am child, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);
sleep(1);
--cnt;
}
exit(1);
}
else
{
//pid_t ret = wait(NULL);
InitTask();
//AddTask(task1);
//AddTask(task2);
//AddTask(task3);
//轮询
while (1)
{
int status = 0;
//pid_t ret = waitpid(id, &status, 0);
pid_t ret = waitpid(id, &status, WNOHANG); //非阻塞
if (ret > 0)
{
//printf("wait success, ret: %d, status: %d, exitsig: %d, exitcode: %d\n", ret, status, status&0x7F, (status >> 8) & 0xFF );
if (WIFEXITED(status))
{
printf("进程是正常跑完的,退出码: %d\n", WEXITSTATUS(status));
}
else
{
printf("进程出异常了\n");
}
break;
}
else if (ret < 0)
{
printf("wait failed!\n");
break;
}
else
{
//ret == 0
//printf("子进程还没有退出,我再等等...\n");
//sleep(1);
//启动父进程自己的任务
ExecuteTask();
usleep(500000);
}
}
sleep(3);
}
return 0;
}
5. wait / waitpid原理
僵尸子进程可以丢掉代码和数据,但是绝不能丢掉代码控制快task_struct,因为task_struct内部有 sigcode、exitcode 两个字段值记录子进程退出时的退出码和异常信号。
因为操作系统不相信用户,并且进程之间具有独立性,所以 wait、waitpid本质上都是由操作系统先判断子进程是否为僵尸进程,再完成读取子进程的task_struct内核数据结构,并将进程的Z状态改为X状态
进程等待失败的情况就是该进程不是父进程的子进程
四、进程程序替换
1. 替换原理
用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变
如果要让子进程执行不同的代码,就要用到程序替换技术
例如,单进程的进程程序替换
./mycommand 后,操作系统为该进程创建PCB、进程地址空间、页表,将程序对应的代码和数据从硬盘加载到内存中,完成页表的映射,当程序执行到 execl 函数时(例如ls),那么操作系统会将磁盘中ls对应的代码和数据直接替换mycmmand进程的代码和数据(包括堆栈等等,但是不替换环境变量),但是PCB和进程地址空间等结构不变,再从 ls 起始代码开始执行,也就是说使用 execl 后,execl 后面的代码将不再执行
当父进程fork创建子进程后,让子进程执行execl,那么子进程的程序替换会有影响到父进程吗?
答案是不会!因为进程具有独立性,可是我们一致认为代码是只读的,他怎么会被修改替换呢?因为这件事并不是绝对的!对于 execl 而言,它的操作者是操作系统,在替换代码时,发现是代码是只读权限,那么操作系统就会触发写时拷贝,开辟新空间存放代码,再修改页表的映射
子进程被创建初始即为13853,直至被wait回收也是13853,这就证明了execl并不是创建新的进程
如果不是父子进程场景,那么直接替换;如果是父子进程场景,那么触发写时拷贝。
注意:
- 程序替换并不会创建新的进程,只是将代码和数据进行了替换
- 程序替换成功后,exec*(表示exec这一系列函数)后续的代码不会执行,因为已经被覆盖替换;exec*函数只有失败返回值,成功就没有返回值
CPU是如何知道新替换的程序入口地址是什么?
涉及编译原理,Linux中形成的可执行程序都是有格式的——ELF,在可执行程序最开始有一个表,表明该可执行程序有哪些段(代码段、数据段等等)并写好地址,可执行程序的入口地址就在表头中
所以,操作系统在替换进程的代码和数据的同时,也获取了该可执行程序的其他信息(程序执行的入口地址)
程序替换后,父进程怎么等待子进程?
替换后父子关系不变,父进程等待的是PCB,PCB没变,替换的程序也会有相应的进程终止信息,所以没有影响
2. 7个替换函数
三号手册:6个库函数
二号手册:1个系统调用
六个库函数底层是都调用该系统调用,完成程序替换
我们主要讲解其中五个:
#include <unistd.h>
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ...,char *const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
execl
execl
函数在当前进程中加载并运行指定的程序,替换当前进程的映像为新程序的映像。如果execl
函数成功执行,那么当前进程将不再执行后续的代码,而是开始执行新程序。如果execl
函数执行失败,它将返回 -1,并设置全局变量errno
以指示错误原因
execl 中的 l 表示的是list,列表的意思,将各个参数像链表一样链接起来,最后指向NULL
它的用法与命令行的用法相同,只是将空格分隔符转为逗号
ls -l -a
execl("/usr/bin/ls", "-l", "-a", NULL)
执行一个程序第一件事就是找到程序所在位置,而第一个参数的意义就是找到程序的路径,在哪个绝对路径执行哪个指令;剩下的参数就是告诉bash命令行怎么执行
注意:
execl
函数不会继承调用它的进程的环境变量,除非显式地通过其他方式(如execve
函数)传递它们。- 如果
execl
函数执行失败,它通常会返回 -1 并设置errno
。常见的错误原因包括文件不存在、文件不是可执行文件、权限不足等。- 由于
execl
函数的参数是变参的,因此在编写代码时需要特别注意参数列表的结束标志 NULL
execlp
execlp
函数在当前进程中查找并执行指定的程序。它首先会检查 PATH 环境变量,以确定要执行程序的位置。找到程序后,它会替换当前进程的映像(包括代码和数据段)为新程序的映像,并开始执行新程序。如果execlp
函数成功执行,那么当前进程将不再执行后续的代码,而是开始执行新程序。如果execlp
函数执行失败,它将返回 -1,并设置全局变量errno
以指示错误原因
这里的p指的是PATH环境变量,它会自动去默认的PATH环境变量中查找
execlp("ls", "ls", "-a", "-l", NULL);
第一个ls参数是让要找到的程序,表示要执行谁,后面的ls表示要执行什么 ,怎么执行
- file:要执行的新程序的名称(不是路径),该函数会在 PATH 环境变量指定的目录列表中查找该程序。
- arg:传递给新程序的参数列表,第一个参数
arg
通常被新程序作为argv[0]
(即程序名称),后续参数是新程序的参数,列表必须以 NULL 指针结束。注意,这里的参数是变参(variadic arguments),意味着你可以传递任意数量的参数,直到遇到 NULL 为止。然而,在实际编程中,由于 C 语言不支持直接传递可变数量的参数并在函数内部识别它们的结束,因此通常最后一个参数使用(char *)NULL
(或简单地NULL
,如果编译器可以隐式转换)来明确指示参数列表的结束。但请注意,在某些情况下,可能需要显式地强制类型转换NULL
为(char *)NULL
,以避免类型不匹配的问题。然而,现代编译器通常能够处理这种类型转换,因此直接使用NULL
也是可以的。
注意:
- 第一个参数(即程序名称)可以写任何内容,但通常写为程序的实际名称以便于日志记录或错误处理。然而,这个名称并不会影响程序的执行路径,因为
execlp
会根据 PATH 环境变量来查找程序。- 参数列表必须以 NULL 指针结束,以指示参数列表的结束。
- 如果
execlp
函数执行失败,它将返回 -1 并设置errno
。常见的错误原因包括文件不存在(即 PATH 环境变量中没有找到指定的程序)、权限不足等
execv
execv函数用于在当前进程中加载并运行指定的程序,替换当前进程的映像(包括代码和数据段)为新程序的映像。如果execv函数成功执行,那么当前进程将不再执行后续的代码,而是开始执行新程序。如果execv函数执行失败,它将返回-1,并设置全局变量errno以指示错误原因
这里的v指的是vector,第二个参数是字符串指针数组,就是命令行参数,这个参数就是要让我们自己主动填写指针数组,再将指针数组传进函数内(我们在命令行参数时已经讲过argv),注意最后一个数据要填NULL
- file:要执行的新程序的名称(不是路径),该函数会在 PATH 环境变量指定的目录列表中查找该程序。
- arg:传递给新程序的参数列表,第一个参数
arg
通常被新程序作为argv[0]
(即程序名称),后续参数是新程序的参数,列表必须以 NULL 指针结束。
第一个参数依旧是指定路径,第二个参数依旧是怎么执行。
操作系统会将 argv 传到 ls 内的main函数的argv表,形成指令选项,exev与execl的区别就是exev我们直接编写好了指针数组argv,而execl还要操作系统帮忙
execv就是加载器,将我们的可执行程序从磁盘导入到内存,也能传递命令行参数,将形参接收的argv传递给其他main函数
注意:
- execv函数不会继承调用它的进程的环境变量,除非显式地通过其他方式(如execve函数)传递它们。
- 如果execv函数执行失败,它通常会返回-1并设置errno。常见的错误原因包括文件不存在、文件不是可执行文件、权限不足等。
- execv函数的参数argv数组必须以NULL结尾,且数组中的每个元素都应该是有效的字符串
execvp
同execv,只是将路径改为直接在PATH环境变量中搜索
make形成多个可执行程序
.PHONY
是 Makefile 中使用的一个特殊目标(target),它用于声明该目标是一个“伪目标”(phony target)。伪目标并不是一个真实的文件名,Makefile 不会对它进行文件存在性的检查,也就是说,伪目标总是会被执行,无论它是否比它的依赖新。使用
.PHONY
的目的是为了明确地告诉 make 工具,某个目标是“伪”的,避免因为目标名称恰好与某个文件名相同而导致 make 工具产生误解。这对于定义一些只执行命令而不产生文件的清理操作(如clean
、distclean
等)特别有用。
- make:设置伪目标all,all 依赖 mycommand 和 otherExe,所以make时会自顶向下寻找第一个依赖关系,all 关系链依赖两个文件,所以此时 make 会编译两个文件,又因为all 没有依赖方法,所以两个文件推导完成后直接结束。如果不设置伪目标all,那么make时只会自顶向下找到第一个依赖关系,例如mycommand,它的依赖文件存在,所以执行完依赖方法就结束了,不会再编译生成otherExe
- clean:rm后直接跟两个可执行程序名即可
execle
既然exec系列函数可以调用系统命令,那么他能调用我们的命令吗?(我们自己的可执行程序)
答案是可以的!我们在 mycommand.c 中通过 execl 调用了otherExe.cpp 可执行程序
可能有疑问的地方就是我们说过命令行怎么写,我们就怎么传参,那么我们之前在命令行执行可执行程序都是 ./otherExe 那么为什么此时 execl 的第二个参数没有带 ./ 呢?
这是因为之前加 ./ 是为了让系统能找到该程序所在位置,知道工作路径在哪,如今 execl 的第一个参数目的就是为了让系统找到该可执行程序所在位置,这个工作已经做完了,所以可以不用带 ./,当然加上了也没有错
那么,C能调用C++程序,同样的C也能调用java、python、脚本等语言编写的程序。
例如,调用脚本文件(需要用解释器bash执行命令——bash test.sh)
无论是可执行程序还是脚本,为什么 execl 能跨语言调用呢?
exec
系列函数本身并不关心所加载的程序是用什么语言编写的。它们只负责将指定的程序文件(如可执行文件或脚本,如果系统配置了相应的解释器)加载到当前进程的地址空间中,并从该程序的入口点开始执行。这意味着,无论是用C、C++、Python、Java还是其他任何语言编写的程序,只要它们被编译或解释为可在当前操作系统上运行的格式,就可以通过exec
系列函数来执行
因为所有的语言运行起来,本质都是进程 ,只要是进程都可以被调度,就可以使用execl替换调用
ps:所以我们就可以在某些地方挂羊头卖狗肉(execl调用别的代码)
趁此机会,再验证一下execv
替换后,环境变量会发生什么?
环境变量也是默认传递的,因为环境变量也是数据,也在进程地址空间,创建子进程后,子进程拷贝父进程的进程地址空间,所以环境变量就被子进程继承下去了
又我们之前再环境变量处讲过一个第三方的全局变量char** environ,它已经被父进程初始化了,创建子进程后,它也被子进程继承下去了,因为子进程并不修改该值,只是简单的使用查询,所以也就不会触发写时拷贝。
所以我们不传参,该进程也能拿到环境变量。但是execl程序替换之后,它替换了代码和数据但唯独没有替换环境变量,即环境变量没有被替换,而是被保留下来了!
新增环境变量
所以如果想给子进程传递环境变量,该怎么传递?
方法一:直接在bash的环境变量内添加环境变量
因为mycommand和otherExe都是bash的子进程,所以它们都继承了bash的环境变量,会让子进程都获取到
方法二:在父进程内部调用 putenv
即直接在父进程的环境变量内部添加,而bash内部没有,查不到
bahs内部没有
方法三:execle搭配environ
覆盖环境变量
方法:execle
注意函数细节,NULL不要忘
此方法直接覆盖原环境变量
小结:
这些函数原型看起来很容易混,但只要掌握了规律就很好记。l(list) : 表示参数采用列表v(vector) : 参数用数组p(path) : 有 p 自动搜索环境变量 PATHe(env) : 表示自己维护环境变量 - 事实上,只有execve是真正的系统调用,其它五个函数最终都调用 execve,所以execve在man手册 第2节,其它函数在man手册第3节。这些函数之间的关系如下图所示。