当前位置: 首页 > news >正文

Linux->进程控制(精讲)

一:fork函数的写时拷贝

1:代码共享

我们知道fork之后,父子进程代码共享,数据则写时拷贝,代码共享我们已经知道了,例子如下:

// proc.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>int main()
{printf("Before:PID is %d\n", getpid());pid_t id = fork();if(id == -1){printf("fork error!\n");exit(1);}printf("After:PID is %d, return is %d\n", getpid(), id);sleep(1);return 0;
}

运行结果 :

Q1:为什么会代码共享?

A1:fork之后子进程会有自己的PCB,进程地址空间,页表;这些内核数据结构的内容拷贝于父进程的内核数据结构,所以一开始进程地址空间通过页表映射的内存地址是一样的,所以父子进程都指向了内存中的那份代码;所以代码共享就是天然共享!

但其实讲到后面第四大点的时候,我们会发现代码也会写时拷贝!


2:写时拷贝

Q2:那写时拷贝是什么意思?

A2:由A1可知,一开始其实子进程的数据也是和父进程共享的,但是父子进程的数据是各自存储的,互相不能影响的(进程具有独立性),但是子进程也有可能不会修改父进程的数据,所以操作系统只会在父子进程中的一个进程修改了数据的时候,会让这个修改数据的进程指向新的内存空间,而没有修改数据的进程还是指向的就的内存空间;这就叫写时拷贝

Q3:为什么不在创建子进程的时候,直接把数据全部给子进程一份?

A3:子进程不一定全用(甚至不用),且不一定现在就要用,(你父进程申请了一个大数组 20mb空间,难道上来就给子进程来一份?)OS主打的就是效率,和节省资源,所以OS只会按需分配,什么时候知道子进程需要呢?就是你子进程进行修改操作的时候!而且无脑给数据 ,只会让创建子进程变慢!

Q4:写时拷贝我理解了,那比如子进程修改数据的时候,为什么还要把父进程旧的内容拷贝到子进程新的空间?

A4:因为可能只是对新的空间,进程局部的修改!不要理解修改就是100%的修改,比如父进程有一个50个int大小的数组,你子进程只是修改其中一个元素,此时就能体现出拷贝旧内容的价值

注意:

①:父进程有两个数组A和B,子进程对其中一个数组A进行修改,则子进程对应的内存会写时拷贝去开辟一个A数组,但是数组B不会影响到....以此类推,栈 堆 也不会互相影响,这就是写时拷贝的强大之处

②:写时拷贝,不一定只是写,指的是任何对数据的修改动作(增删查改),都能触发写时拷贝

③:父子进程任意一方修改数据,都会触发写时拷贝,而不是只有子进程修改才会触发

下图是对1和2的总结:

解释:子进程在修改数据后,指向的新的内存空间 


3:页表的权限

3权当做知识拓展即可;我们的页表其实除开对虚拟地址有着映射到物理地址的功能,还能其他汗多功能,其一就为权限功能:

我们都知道无法一个字符串常量应该用const char * 来接收,如果仅用char*来接收,则会警告:

解释:第一张如第五行代码前的黄色W就是警告的意思;所以我们良好的编程习惯都是在前面加上const

现在我们分别对两种写法进行第10行注释的解开:

解释:有const的会报错,这是因为加了const会在编译阶段就发现你企图修改一个只读的变量,然后就立即停止,所以连my.exe都没有生成;而没加const,则会生成my.exe后再报错,这叫运行时报错,报错是段错误;修改一个字符串常量的时候,就会触发段错误

const 并不能够阻止段错误,const是你提前告诉编译器,这个变量不能修改,若后面修改,编译器会提前给你报错,这是编译器会在运行之前检查出来的,你加了 ,本质是让你运行时报错,在编译期间就被发现了,加const是一种防御性编程,这是一种好的编程习惯

也会有一种情况:发现,打印的效果一样,并且均触发了"Segmentation fault"段错误,这是因为你Linux所使用的C版本过于低了导致的:

  • 旧版 C 标准(如 C89):允许 char* str = "Hello";(不强制 const),修改时仅运行时崩溃。

  • 现代 C 标准(如 C11)const char* str = "Hello"; 如果尝试修改,应该直接编译报错

Q:那到底是什么阻止了字符串常量被修改?

A:页表的权限功能,当你想修改一个变量的时候,本质是在内存中找到再修改,而当你试图通过页表去找到物理地址的时候,此时OS检测到你试图w一个只有r权限的物理地址,所以,驳回!

总结:

不要误以为一个变量不能被修改,是const起到的作用,const只是让程序在编译的时候,就会报错;所以,一个变量不能够修改,其实是OS阻止了你,跟语言没关系!

二:进程终止

进程终止和进程退出的关系:后者是前者的子集

1:进程主动退出

①:main中的return 0

最常见的进程主动退出就是main函数中的return 0!

Q:为什么c/c++main函数最后都要return 0?

A:main函数的return 0的这个0叫作进程的退出码 ,0代表进程主动退出成功,非0代表进程主动退出失败!

注:0比较特殊,所以规定0代表进程主动退出成功;No why ~

指令补充:echo $?指令可以获取最近一个进程的退出码

 

下图用echo $?指令打印出一段程序的退出码:

解释:这个指令不存在,所以OS在PATH环境变量的路径找不到,报错无文件或目录,我们再通过 echo $?指令 得知其退出码为2,我们通过一个编号,也不知道到底是为什么错,所以需要转换一下;而strerror函数就能将退出码转换为错误信息

用strerror函数来转换即可:

打印如下:

[wtt1@hcss-ecs-1a2a ~]$ ./my.exe
0: Success
1: Operation not permitted
2: No such file or directory
3: No such process
4: Interrupted system call
5: Input/output error
6: No such device or address
7: Argument list too long
8: Exec format error
9: Bad file descriptor
10: No child processes
11: Resource temporarily unavailable
12: Cannot allocate memory
13: Permission denied
14: Bad address
15: Block device required
16: Device or resource busy
17: File exists
18: Invalid cross-device link
19: No such device
20: Not a directory
21: Is a directory
22: Invalid argument
23: Too many open files in system
24: Too many open files
25: Inappropriate ioctl for device
26: Text file busy
27: File too large
28: No space left on device
29: Illegal seek
30: Read-only file system
31: Too many links
32: Broken pipe
33: Numerical argument out of domain
34: Numerical result out of range
35: Resource deadlock avoided
36: File name too long
37: No locks available
38: Function not implemented
39: Directory not empty
40: Too many levels of symbolic links
41: Unknown error 41
42: No message of desired type
43: Identifier removed
44: Channel number out of range
45: Level 2 not synchronized
46: Level 3 halted
47: Level 3 reset
48: Link number out of range
49: Protocol driver not attached
50: No CSI structure available
51: Level 2 halted
52: Invalid exchange
53: Invalid request descriptor
54: Exchange full
55: No anode
56: Invalid request code
57: Invalid slot
58: Unknown error 58
59: Bad font file format
60: Device not a stream
61: No data available
62: Timer expired
63: Out of streams resources
64: Machine is not on the network
65: Package not installed
66: Object is remote
67: Link has been severed
68: Advertise error
69: Srmount error
70: Communication error on send
71: Protocol error
72: Multihop attempted
73: RFS specific error
74: Bad message
75: Value too large for defined data type
76: Name not unique on network
77: File descriptor in bad state
78: Remote address changed
79: Can not access a needed shared library
80: Accessing a corrupted shared library
81: .lib section in a.out corrupted
82: Attempting to link in too many shared libraries
83: Cannot exec a shared library directly
84: Invalid or incomplete multibyte or wide character
85: Interrupted system call should be restarted
86: Streams pipe error
87: Too many users
88: Socket operation on non-socket
89: Destination address required
90: Message too long
91: Protocol wrong type for socket
92: Protocol not available
93: Protocol not supported
94: Socket type not supported
95: Operation not supported
96: Protocol family not supported
97: Address family not supported by protocol
98: Address already in use
99: Cannot assign requested address
100: Network is down
101: Network is unreachable
102: Network dropped connection on reset
103: Software caused connection abort
104: Connection reset by peer
105: No buffer space available
106: Transport endpoint is already connected
107: Transport endpoint is not connected
108: Cannot send after transport endpoint shutdown
109: Too many references: cannot splice
110: Connection timed out
111: Connection refused
112: Host is down
113: No route to host
114: Operation already in progress
115: Operation now in progress
116: Stale file handle
117: Structure needs cleaning
118: Not a XENIX named type file
119: No XENIX semaphores available
120: Is a named type file
121: Remote I/O error
122: Disk quota exceeded
123: No medium found
124: Wrong medium type
125: Operation canceled
126: Required key not available
127: Key has expired
128: Key has been revoked
129: Key was rejected by service
130: Owner died
131: State not recoverable
132: Operation not possible due to RF-kill
133: Memory page has hardware error
134: Unknown error 134
135: Unknown error 135
136: Unknown error 136
137: Unknown error 137
138: Unknown error 138
139: Unknown error 139
140: Unknown error 140
141: Unknown error 141
142: Unknown error 142
143: Unknown error 143
144: Unknown error 144
145: Unknown error 145
146: Unknown error 146
147: Unknown error 147
148: Unknown error 148
149: Unknown error 149
150: Unknown error 150
151: Unknown error 151
152: Unknown error 152
153: Unknown error 153
154: Unknown error 154
155: Unknown error 155
156: Unknown error 156
157: Unknown error 157
158: Unknown error 158
159: Unknown error 159
160: Unknown error 160
161: Unknown error 161
162: Unknown error 162
163: Unknown error 163
164: Unknown error 164
165: Unknown error 165
166: Unknown error 166
167: Unknown error 167
168: Unknown error 168
169: Unknown error 169
170: Unknown error 170
171: Unknown error 171
172: Unknown error 172
173: Unknown error 173
174: Unknown error 174
175: Unknown error 175
176: Unknown error 176
177: Unknown error 177
178: Unknown error 178
179: Unknown error 179
180: Unknown error 180
181: Unknown error 181
182: Unknown error 182
183: Unknown error 183
184: Unknown error 184
185: Unknown error 185
186: Unknown error 186
187: Unknown error 187
188: Unknown error 188
189: Unknown error 189
190: Unknown error 190
191: Unknown error 191
192: Unknown error 192
193: Unknown error 193
194: Unknown error 194
195: Unknown error 195
196: Unknown error 196
197: Unknown error 197
198: Unknown error 198
199: Unknown error 199

解释:发现退出码为2的错误信息正是"No such file or directory",正好吻合;其中的0代表"success"也就是成功,这也是我们main中return 0的意义;

总结:main函数return返回的时候,表示进程退出,通过echo $?指令可以获取到退出码,获取到的退出码不一定是0,可以strerror函数可以将退出码转换为对应的退出信息

②:exit()函数 和 _exit()函数

一个进程出来在main函数中进行return可以让进程退出,还可以用exit()函数或者_exit()函数在进程的任意地方进行进程退出(不一定在main函数中,可以在整个程序的非main函数中)

先介绍一下函数:exit()函数的参数是一个整形,返回值就是这个整形,返回值是退出码,类似与main的return 0,我们可以随便的设置exit()函数的参数,但是我们一般默认和return 0一样设置为exit(0),这代表退出成功,如果退出不成功了,则退出码不再是0,其会自己改变,不用担心

Q:什么时候exit()函数的参数需要手动设置?

A:(不同非零退出码可以区分不同类型的失败,方便调试和自动化处理)

if (file_not_found) {exit(10);  // 自定义:文件不存在
} else if (permission_denied) {exit(20);  // 自定义:权限不足
}

证明一下exit函数可以进程退出:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>void Print()
{printf("hello Print\n");exit(0); // 调用 exit(4) 终止进程,退出码为 0
}int main()
{while(1){printf("I am a process: %d\n", getpid()); // 打印当前进程IDsleep(1); // 休眠1秒Print();  // 调用 Print 函数(内部会终止进程)}return 0; // 实际不会执行到这里(因进程已被 exit(4) 终止)
}

解释:主函数 main 中的 while(1) 会不断循环,因为第一次调用 Print() 函数就被exit函数终止进程,所以循环实际上只会执行一次;如果成功退出进程,通过echo $?指令获取到的就是0,和main的return 0 类似!

exit和_exit函数的唯一区别在于:使用exit函数退出进程前,exit函数会刷新缓冲区,然后再终止进程,而_exit函数会直接终止进程,不会做任何收尾工作。

例子:

解释:暂且理解为\n会刷新缓冲区,所以双方都不加\n ,只有exit把缓冲区刷新了出来!

其实exit封装了_exit ,也就是库函数封装了系统调用函数!库函数为什么封装了系统调用函数,理由已经说过很多遍了,如下:

不同的OS提供的系统调用接口一定是不一样的!所以如果只使用系统调用接口,在Linux上能跑的代码,在windows上肯定跑不了!但是不管在哪个OS上运行C语言的函数,效果都一样,这就是封装的意义;比如exit函数,在不同的OS中封装了不同的系统调用接口,但是又有何关系,我们的exit是统一的!这样侧面体现了为什么说c/c++具有移植性,跨平台性!

至此,就介绍了完了进程主动退出的三种方式

总结:只有在main函数当中的return才能起到退出进程的作用,子函数当中return不能退出进程,而exit函数和_exit函数在代码中的任何地方使用都可以起到退出进程的作用。

2:进程被动终止

进程被动终止又称为进程异常退出

要知道的是,只有进程是主动退出的时候,其的退出码才会有价值;当进程被动终止的时候(收到异常)则退出码无意义!

类比:考试作弊被抓了(进程被动终止)则你的成绩(退出码)无意义

所以需要明白一个进程结束后,不管是主动退出,还是被动退出,其都用该返回两个值,一个叫退出码,一个叫终止信号,这两个东西在进程等待中会重点讲解

进程异常退出的情况:

情况一:向进程发生信号导致进程异常退出。

例如,在进程运行过程中向进程发生kill -9信号使得进程异常退出,或是使用Ctrl+C使得进程异常退出等。

情况二:代码错误导致进程运行时异常退出。

例如,代码当中存在野指针问题使得进程运行时异常退出,或是出现除0的情况使得进程运行时异常退出等。

所以至此,可以总结出一个进程终止的三种情况:

①:代码运行完毕,结果正确    ->考试 过了
②:代码运行完毕,结果不正确 ->考试 没过 
③:代码异常终止 ->作弊被抓 

三:进程等待

进程等待的必要性:

子进程退出,父进程如果不读取子进程的退出信息,子进程就会变成僵尸进程,进而造成内存泄漏。所以父进程必须要等待子进程退出,然后去读取子进程的退出信息!
 

注:进程一旦变成僵尸进程,就算是kill -9命令也无法将其杀死

 进程等待所使用的两个函数wait和waitpid:

pid_t wait(int*status);返回值:成功返回被等待进程pid,失败返回-1。
参数:输出型参数,获取子进程退出状态,不关心则可以设置成为NULL
pid_ t waitpid(pid_t pid, int *status, int options);返回值:当正常返回的时候waitpid返回收集到的子进程的进程ID;出错,则返回-1pid:Pid=-1,等待任一个子进程。与wait等效。Pid>0.等待其进程ID与pid相等的子进程。status:存储退出码和终止信号options:默认为0,进行的阻塞等待;反之为WNOHANG进行的是非阻塞等待! 

解释:

①:二者都有一个status参数,该参数是一个输出型参数,由操作系统进行填充,填充的就是我们上面说的退出码和终止信号!如果对status参数传入NULL,表示不关心子进程的退出状态信息

②:阻塞等待:子进程还未退出,则父进程一直等待子进程退出

       非阻塞等待: 查一下 子进程没退出 立即返回(不再等待),所以非阻塞一般都是多次访问,叫作基于非阻塞的轮询访问

例子:李四让张三辅导功课,张三正在下楼,李四在楼下等张三;若是阻塞等待,则李四在张三下楼期间,打电话询问张三是否已经完成下楼,张三说没有,则李四会不挂电话,且原地等待;若是非阻塞等待,则李四打电话询问张三,张三说没有,则李四挂断电话,边等待变抽空做自己的事情,然后再打电话询问,若是还没有,则还是挂断电话,边等边做自己的事情,再打,发现下楼了,(这就叫基于非阻塞的轮询访问,一个循环体中,多次调用waitpid函数)

重点在于int *status指向的整形空间,如何存放退出码和终止信号两个整形值?如下:

status是一个整型变量,但status不能简单的当作整型来看待,status的不同比特位所代表的信息不同,具体细节如下(只研究status低16比特位,其中core dump位不用管):

在status的低16比特位当中,高8位表示进程的退出状态,即退出码。进程若是被信号所杀,则低7位表示终止信号,而第8位比特位是core dump标志(不用管):

所以,要获得这两个值,我们自然而然的想到移位操作!

exitCode = (status >> 8) & 0xFF; //退出码
exitSignal = status & 0x7F;      //退出信号//0x7F:0000 0000 0000 0000 0000 0000 0111 1111
//0xFF:0000 0000 0000 0000 0000 0000 1111 1111

对于此,系统当中提供了两个宏来获取退出码和退出信号。

  • WIFEXITED(status):用于查看进程是否是正常退出,本质是检查是否收到信号。
  • WEXITSTATUS(status):用于获取进程的退出码。
exitNormal = WIFEXITED(status);  //是否正常退出
exitCode = WEXITSTATUS(status);  //获取退出码

1:wait函数的使用方法:

#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){//childint count = 10;while(count--){printf("I am child...PID:%d, PPID:%d\n", getpid(), getppid());sleep(1);}exit(0);}//fatherint status = 0;pid_t ret = wait(&status);if(ret > 0){//wait successprintf("wait child success...\n");if(WIFEXITED(status)){//exit normalprintf("exit code:%d\n", WEXITSTATUS(status));}}sleep(3);return 0;
}

通过该指令进行监视: 

[cl@VM-0-15-centos procWait]$ while :; do ps axj | head -1 && ps axj | grep proc | grep -v grep;echo "######################";sleep 1;done

这时我们可以看到,当子进程退出后,父进程读取了子进程的退出信息,子进程也就不会变成僵尸进程了。 

2:waitpid函数阻塞写法:

代码核心功能

  • 创建子进程:通过 fork() 分裂出父子两个进程。

  • 子进程任务:循环打印10次自身信息(PID和父进程PPID),然后正常退出(exit(0))。

  • 父进程任务:通过 waitpid() 阻塞等待子进程结束,并分析子进程的终止状态。

#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 = waitpid(id, &status, 0);if (ret >= 0){//wait success                    printf("wait child success...\n");if (WIFEXITED(status)){//exit normal                                 printf("exit code:%d\n", WEXITSTATUS(status));}else{//signal killed                              printf("killed by siganl %d\n", status & 0x7F);}}sleep(3);return 0;
}

 

3:waitpid函数的非阻塞写法:

代码核心功能

  • 创建子进程:通过 fork() 分裂出父子两个进程。

  • 父进程在子进程运行的期间进行基于非阻塞的轮询访问,访问得知子进程还未退出,则自己做自己的事情("father say: child is running, do other thing\n"),等待成功则("wait success, rid: %d, status: %d, exit code: %d\n")

#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) {// 子进程代码int cnt = 5;while (cnt) {printf("I am child process, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);cnt--;sleep(1);}exit(111); // 子进程退出码设为111}// 父进程代码(非阻塞等待,与图中逻辑一致)int status = 0;while (1) {pid_t rid = waitpid(id, &status, WNOHANG); if (rid > 0) {printf("wait success, rid: %d, status: %d, exit code: %d\n", rid, status, WEXITSTATUS(status));break;} else if (rid == 0) {printf("father say: child is running, do other thing\n");} else {perror("waitpid");break;}}return 0;
}

注意:

1:

Q: 为什么不直接定义两个全局变量来存储退出码和终止信号,然后子进程自己退出前赋值给全局变量呗,这样你父进程去拿全局变量呗?

A:进程具有独立性,父子进程之间无法直接修改对方的数据,全局变量你定义在main外,就是属于父进程的,此时你子进程修改的只是自己的全局变量!所以只能通过系统调用接口waitpid中的status来看!所以waitpid函数是把内核的数据拷贝到了status变量中,系统调用接口才能访问到内部数据,合理!且两个值退出码和终止信号均存在于PCB中,如图(Linux源码):

2:一般不会直接去读取退出码和终止信号这两个值,而是终止信号为0(无异常),才会去读取退出码

3:我们一般使用的都是waitpid函数,因为其可以选择非阻塞等待,比较灵活

       

四:进程替换

1:原理

进程替换不会产生新的进程,因为仅仅替换掉了一个旧进程在内存中的数据和代码,导致旧的物理内存当中的数据和代码发生了改变;PCB是旧的,进程地址空间是旧的,页表也是旧的,最重要的事PID也没变,所以不算产生新进程

而进程替换,一般和fork函数结合,交给子进程去进行替换,这也是我们使用的shell的本质,shell之所以能够执行各种命令,本质就是shell的一号进程bash,去创建子进程,让其进行进程的替换:

2:单进程替换进程

进程替换会影响内存的数据,所以注定了需要函数调用来实现,共有7种函数全是exec开头的,我会介绍其中的5中:

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函数写一个单进程替换的例子:

int execl(const char *path, const char *arg, ...);
//第一个参数是要执行程序的路径,第二个参数是可变参数列表,表示你要如何执行这个程序,并以NULL结尾
#include <stdio.h>
#include <unistd.h>int main() {printf("I am a process, pid: %d\n", getpid());printf("exec begin...\n");execl("/usr/bin/ls", "ls", "-a", "-l", NULL); // 注意:execl 不是 exec1printf("exec end ...\n"); // 若 execl 成功,此行不会执行return 0;
}

运行结果:

 从这个例子,要说4个点:

细节1:进程替换之后,exec*系列的接口后续的代码不再执行,因为不属于替换后的进程!(所以  printf("exec end ...\n");没有执行)

细节2:exec系列的接口只有失败才会返回值,成功直接执行替换后的程序,何来时间何来机会返回值?只有失败(找不到即将替换的程序),此时退出码为1

细节3:所有exec系列的接口,你第二个参数若是const char *arg,则一定是NULL结尾,不是"NULL",其次你若是char *const argv[ ],这个可变参数数组本身就要求是NULL结尾,所以二者其实结尾是一致的

细节4:"创建一个进程是先创建PCB 地址空间 页表等,再把程序加载到内存,因为先加载程序到内存,页表都没办法映射,且OS可以通过缺页中断动态调用代码的(证明动态调用那部分代码没有先加载到内存中),而替换本质不就是加载程序这一步吗?所以替换没什么神奇的,只是OS基操
 

你若是替换的进程是top指令,你可以通过相关指令发现。替换前后的进程的pid一样,这也是为什么说进程替换无新进程的产生

3:多进程替换进程

多进程替换进程,就是最常用的进程替换的方法,创建一个子进程,让子进程进程进程替换工作:

例子:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>int main()
{printf("I am a process, pid: %d\n", getpid());pid_t id = fork();if (id == 0){// 子进程sleep(3);printf("exec begin...\n");execl("/usr/bin/ls", "ls", "-a", "-l", NULL); // 注意:execl 不是 exec1//execl("/usr/bin/top", "top", NULL); // 备用命令(图中被注释)printf("exec end ...\n"); // 若 execl 成功,此行不会执行exit(1);}// 父进程pid_t rid = waitpid(id, NULL, 0);if (rid > 0){printf("wait success\n");}exit(1);
}

解释:进程替换相关函数,一般是和waitpid函数结合使用;之前我们说过fork只有数据会写时拷贝,其实这里也体现了代码也会写时拷贝,所以这里推翻了原有结论,在某些时候,子进程也会写时拷贝父进程的代码,就比如这种子进程的代码即将被替换的时候!!(只不过这里的写时拷贝,没有体现保留部分父进程的代码罢了)

Q:所以shell是如何运行起一个指令的?
A:
很简单。shell fork创建子进程,然后子进程进行进程替换,这个即将替换上去的进程,就是指令对应的程序,然后shell进程(父进程)在外waitpid,所以此时也再次证明了为什么是创建一个进程是"先创建PCB 地址空间 页表等,因为再进程替换中子进程一开始根本单独没有数据和代码,都是指向的父进程的,后面才会写时拷贝代码(替换程序的代码)!

4:exec接口

需要学习的exec类接口:

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[]);

这五个exec系列函数的函数名都以exec开头,其后缀的含义如下:

  • l(list):表示参数采用列表的形式,一一列出。(上面的ls例子)
  • v(vector):表示参数采用数组的形式。
  • p(path):表示能自动搜索环境变量PATH,进行程序查找。
  • e(env):表示可以传入自己设置的环境变量。

所以了解了这5种,OS提供的7种进程替换的接口也都会了:

函数名参数格式是否带路径是否使用当前环境变量
execl列表
execlp列表
execle列表否,需自己组装环境变量
execv数组
execvp数组
execve数组否,需自己组装环境变量

事实上,只有execve才是真正的系统调用,其它五个函数最终都是调用的execve,所以execve在man手册的第2节,而其它五个函数在man手册的第3节,也就是说其他五个函数实际上是对系统调用execve进行了封装,以满足不同用户的不同调用场景的。

下图为exec系列函数族之间的关系: 

举例execv函数:

int execv(const char *path, char *const argv[]);
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>int main()
{printf("I am a process, pid: %d\n", getpid());pid_t id = fork();if(id == 0){char *const argv[] = {(char*)"ls",(char*)"-a",(char*)"-1",NULL};sleep(3);printf("exec begin...\n");execv("/usr/bin/ls", argv);//execl("/usr/bin/ls", "ls", "-a", "-1", NULL); //NULL 不是 "NULL"//execlp("ls", "ls", "-a", "-1", NULL); //NULL 不是 "NULL"//execl("/usr/bin/top", "/usr/bin/top", NULL); //NULL 不是 "NULL"printf("exec end ...\n");exit(1);}pid_t rid = waitpid(id, NULL, 0);if(rid > 0){printf("wait success\n");}exit(1);
}

 

总结:

1:

所以,其实几种函数都是大同小异的,理解每个参数的意义,以及凭借函数名想起参数才是重要的;所以main函数不是天生就是可变参数列表和环境变量数组,这都是上级进程产生子进程,让子进程进行进程替换,采取的是execle函数!

int execle(const char *path, const char *arg, ...,char *const envp[]);

2:

几exec函数如果有 const char *arg, ... 参数,建议这样使用:execlp("ls","ls","-a","-1",NULL);第一个参数的ls和第二个参数的ls意义不一样,不要省去第二个参数的ls,尽管能够运行,但是按照标准来总没错

3:

exec系列的接口,可以执行进程替换我们自己写的进程(程序);无论是什么语言 只要能在Linux下跑 就都可以通过exec系列的接口调用! 本质就是什么语言写的运行起来都变成了进程 进程替换当然可以!!对于OS语言无差别,仅需替换数据和代码即可!

4:

当使用 exec 系列函数(如 execvexeclexecvp 等)进行进程替换(Process Replacement)时,新程序会覆盖原进程的代码段、数据段、堆栈等内存空间,但环境变量(environment variables)默认会被保留,除非显式指定替换。

例如,你的代码中:

execv("/usr/bin/ls", argv);  // 用 /usr/bin/ls 替换当前进程

执行后,原进程的代码(如 printf("exec end ...\n");)会被 ls 程序的代码覆盖,但环境变量默认仍然存在。 

五:环境变量的传递方式

1. 子进程继承全部环境变量

  • 方法:直接 fork() 创建子进程。

  • 特点:子进程会自动复制父进程的全部环境变量(environ),无需额外操作。

2. 新增环境变量(影响当前进程及子进程)

  • 方法:使用 putenv 或 setenv 添加环境变量。

  • 特点

    • 修改当前进程的环境变量。

    • 后续 fork() 的子进程会继承修改后的环境变量。

3. 仅子进程使用全新环境变量(不影响父进程)

  • 方法:使用 execle 或 execve,并显式传递环境变量表

  • 特点

    • 覆盖式传递:子进程仅使用指定的环境变量,不继承父进程的环境变量。

    • 不影响父进程:父进程的环境变量保持不变。

示例

char *const envp[] = {"MY_VAR=123", "PATH=/custom/bin", NULL};
execle("/bin/ls", "ls", "-l", NULL, envp);  // 仅子进程使用 envp

总结对比

需求场景方法影响范围
子进程继承全部环境变量fork()子进程完全复制父进程环境
新增环境变量(父子共享)putenv / setenv当前进程 + 后续子进程
仅子进程使用全新环境变量execle / execve仅子进程(覆盖式传递)

这样修改后,逻辑更清晰,重点更突出,便于理解不同场景下的环境变量传递机制。

最后感谢博主2021dragon的部分博客材料!❀

http://www.lqws.cn/news/591931.html

相关文章:

  • 文心一言开源版测评:能力、易用性与价值的全面解析
  • 通过http调用来访问neo4j时报错,curl -X POST 执行指令报错
  • 博途多重背景、参数实例
  • swing音频输入
  • 跨境证券交易系统合规升级白皮书:全链路微秒风控+开源替代,护航7月程序化交易新规落地
  • 7.可视化的docker界面——portainer
  • CloudBase AI ToolKit实战:从0到1开发一个智能医疗网站
  • LLM中的思维链,仅仅在提示词中加上思维链,让模型输出,和加上思维链限制模型输出答案,这两方式模型是不是进行了思考的
  • 鸿蒙Next开发中三方库使用指南之-nutpi-privacy_dialog集成示例
  • 用“做饭”理解数据分析流程(Excel三件套实战)​
  • 网站崩溃的幕后黑手:GPTBot爬虫的流量冲击
  • 论文阅读:Align and Prompt (ALPRO 2021.12)
  • 零开始git使用教程-传html文件
  • 浅谈Docker Kicks in的应用
  • 51单片机制作万年历
  • 观察者模式
  • 新版本 Spring Data Jpa + QueryDSL 使用教程
  • TensorFlow源码深度阅读指南
  • 【科研绘图系列】基于R语言的复杂热图绘制教程:环境因素与染色体效应的可视化
  • C#程序设计简介
  • 9-2 MySQL 分析查询语句:EXPLAIN(详细说明)
  • Milvus docker-compose 部署
  • 从苹果事件看 ARM PC市场的未来走向
  • 2025年Java后端开发岗面试的高频项目场景题 + 八股文(100w字)
  • SAFNet:一种基于CNN的轻量化故障诊断模型
  • 【os】标准库
  • Rust 学习笔记:比较数值
  • 分布式锁——学习流程
  • 设计模式精讲 Day 20:状态模式(State Pattern)
  • 从零到一搭建远程图像生成系统:Stable Diffusion 3.5+内网穿透技术深度实战