进程创建
fork()初始
在进程中调用fork()
会创建一个新进程,新进程为子进程,原进程为父进程。
#include <unistd.h>
pid_t fork(void);#创建一个子进程
#返回值:父进程返回子进程的pid,子进程返回0
进程调用fork()
后,内核会做:
- 分配新的内存块和内核数据结构给子进程
- 将父进程的部分数据拷贝到子进程
- 添加子进程到系统进程列表中
fork()
返回,开始调度器调度
当一个进程调用fork()后,内核会创建一个子进程,子进程与父进程从具有相同的二进制代码,并且子进程与父进程都会从fork()之后开始执行。
[sy1@VM-16-11-centos 1_27]$ cat process_control.c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main( void )
{
pid_t pid;
printf("Before: pid is %d\n", getpid());
if ( (pid=fork()) == -1 )
{
perror("fork()");
exit(1);
}
printf("After:pid is %d, fork return %d\n", getpid(), pid);
sleep(1);
return 0;
}
//运行结果:
[sy1@VM-16-11-centos 1_27]$ ./process_control
Before: pid is 9325
After:pid is 9325, fork return 9326
After:pid is 9326, fork return 0
从结果可以看出,父进程9325打印了before和after,子进程9326只打印了fork()之后的after。
[!Important]
fork()
之前,父进程独立运行;fork()
之后,父子进程共同运行。
[!Note]
fork()
创建进程也可能失败,失败的原因:
- 系统中有太多的进程
- 实际用户的进程超过了限制
写时拷贝
在进程地址空间中我们已经介绍过写时拷贝
的概念:
当子进程创建时同时会创建和父进程一样的进程地址空间
和页表
,通过相同的映射关系保证父子进程共享数据。当父子进程其中一个对共享数据进行修改时,OS会在物理内存中对要修改的数据拷贝一份,修改拷贝的数据并更正页表的映射关系。写时拷贝
保证了父子进程其中之一修改数据不会影响到另一个进程,维持了进程之间的独立性。
在这里我们对写时拷贝主要需要理解的是:
- 为什么创建子进程时不直接将父进程的数据全部拷贝给子进程?
OS不知道子进程是否会修改与父进程共享的数据,也不知道何时修改,修改多少……而在这种情况下盲目地将父进程所有数据都在物理内存中拷贝一份给子进程会大大降低OS对资源地利用率,同时也会增加fork()的成本(创建子进程还需要拷贝物理内存上的数据)。所以规定了当子进程尝试对共享数据进行写入,OS才会在内存中拷贝一份数据供子进程修改 - 为什么是写时拷贝而不是写时申请空间呢?
当父子进程尝试对共享数据写入时,不一定是百分百的覆盖,有可能是在原来的数据上进行部分修改(自增、自减等),如果只是写时申请空间而不拷贝数据那么就找不到需要修改的数据了也就不知道在什么基础上进行修改了。 - 写时拷贝的工作具体是如何做到的?
通过页表
做到的,页表中除了包含虚拟地址到物理地址的映射,**还包含了每个虚拟地址的权限,**地址的权限有读权限(r)、写权限(w)、执行权限(x)。定义在r权限上的数据为只读(字符常量区的字符串处于r权限上)。创建子进程时OS会将父子进程页表中数据段对应的权限由rw(可读可写)改为r(只读)。父子进程任意一方尝试对数据段的数据进行写入,由于此处权限为只读,就会出现写入冲突,这时作为软硬件资源的管理则会OS就会出面调解,OS认为数据段的权限本应该是rw,而之前创建子进程设置权限为r就是为了等以后数据段发生写时拷贝能呼出资源管理者OS解决冲突,于是OS在物理内存上对修改的数据进行写时拷贝,重新建立页表的映射关系,并将该数据段上写入的数据权限从r权限改为rw权限。OS故意采用这种让系统出错的办法让OS自己介入,这种错误不是真的错误而是为了发生写时拷贝才设计的。这可以保证对数据的写入次数和触发OS写时拷贝的次数是一样的,保证按需进行写时拷贝。
进程终止
在进程终止中,我们会着重介绍相关进程退出的信息(退出码、退出信号)、进程退出的场景、进程退出的方式。
进程退出的三种场景
一个进程执行结果有三种:
- 进程正常执行完毕,运行结果正确
- 进程正常执行完毕,运行结果不正确
- 进程执行过程中异常终止,结果无意义
上述三种结果都可以通过2个整形变量表示:exit_signal
和exit_code
- exit_signal:进程退出信号。操作系统像进程发送的一种通知,告知进程需要终止。exit_signa为0表示没有收到退出信号,进程正常执行完;exit_signal非0表示进程收到了退出信号,退出信号的含义取决于exit_signal的值。
- exit_code:进程退出码。只有当进程没有收到退出信号即正常执行完毕,进程退出码才有意义。退出码为0表示进程运行结果正确;退出码非0表示运行结果不正确,退出码的含义取决于exit_code的值。
判断一个进程的执行结果:
- 首先看
exit_signal
:若exit_signal
非0,说明进程异常终止,此时运行结果无意义; - 若
exit_signal
为0且exit_code
为0表示进程执行完毕并且运行结果正确;若exit_code
非0表示进程执行完毕但是运行结果不正确。
[!Note]
上述说的运行结果正确是指在运行过程中没有发生阻止其正常操作的严重错误或异常,并不能保证程序的逻辑是正确的。
进程退出码
当一个子进程执行完毕后,会告诉父进程该子进程的执行状况(运行结果正确还是不正确)。通常这个执行状况用退出码
表示,0表示子进程执行完毕正常退出,非0的数字代表运行结果不正确可能的原因。在Linux中,可用echo $?
获取最近一次进程的退出码
。
[sy1@VM-16-11-centos 1_29]$ ls #执行ls进程
makefile process_control process_control.c
[sy1@VM-16-11-centos 1_29]$ echo $? #查看ls进程的退出码
0 #退出码为0,表示ls进程正常退出
[sy1@VM-16-11-centos 1_29]$ jdksadkj #执行一个不存在的指令
-bash: jdksadkj: command not found
[sy1@VM-16-11-centos 1_29]$ echo $? #查看最近一次进程退出码
127 #最近一次执行了一个不存在的进程,而系统找不到指令的退出码在Linux下是127
- 在C语言中,main()函数的返回值是进程的退出码,其他函数的返回值仅仅表示函数执行完毕。
在Linux下,一切皆文件,调用进程本质上是执行用C语言写的可执行程序,main()
函数是C程序的入口,所以当main()
函数返回时,表示该进程已经结束了。main()
函数的返回值就是进程的退出码。
[sy1@VM-16-11-centos 1_29]$ cat process_control.c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main( void )
{
printf("hello exit_code!\n");
return 12;//返回12,进程退出码为12
}
[sy1@VM-16-11-centos 1_29]$ ./process_control
hello exit_code!
[sy1@VM-16-11-centos 1_29]$ echo $?
12
echo $?
命令告诉我们main()
函数的返回值就是调用main()
函数的进程的退出码。
-
退出码0表示进程正常执行完毕;非0表示进程执行完毕,但是执行失败了
-
错误码与退出码:错误码是库函数执行失败后会将全局变量
errno
设置为某个值表示具体失败的原因,通常0表示成功;退出码是子进程结束时返回给父进程的值,告诉父进程子进程执行的结果如何,通常0表示成功。
在某些情况下:库函数调用失败会设置错误码,此时函数的错误码可能会成为程序的退出码。但并不是所有的错误码都直接映射到进程的退出码,因为在程序内部可能会对错误码进行一些处理后才返回给操作系统。
在进程退出码
等于错误码
的情况下,我们通过观察错误码
表示的原因可以知道进程退出码
对应的含义。通过strerrno()
函数获取错误码
对应的原因。
[sy1@VM-16-11-centos 1_29]$ cat process_control.c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
int main( void )
{
printf("hello exit_code!\n");
int i = 0;
for (i = 0; i < 200; i++)
{
printf("%d: %s\n", i,strerror(i));//观察0-200错误码对应的原因
}
return 0;
}
不同错误码对应的原因:
当我使用ls进程打开一个不存在的文件时,ls进程退出码会被设置为2(No such file or directory),与错误码2的含义相同。
[sy1@VM-16-11-centos 1_29]$ ls asdadad
ls: cannot access asdadad: No such file or directory
[sy1@VM-16-11-centos 1_29]$ echo $?
2
进程退出信号
当一个进程尝试访问野指针、数组越界、空指针解引用、发生除零时,进程会收到退出信号,让进程异常退出。进程异常的本质是收到了退出信号!!
通过kill -l
指令查看所有exit_signal
[sy1@VM-16-11-centos 1_29]$ kill -l
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX
每个信号都有一个数字编号,编号0没有对应的信号,0用于表示进程没有收到退出信号。
通过代码模拟除零错误:
int main( void )
{
printf("Process is running!\n");
int a = 1 / 0; //发生除零错误,进程会收到退出信号
return 0;
}
[sy1@VM-16-11-centos 1_29]$ ./process_control
Process is running!
Floating point exception #发生除零错误
通过信号SIGFPE
模拟除零错误
同样地,我们可以通过SIGSEGV
模拟段错误。
进程退出方法
进程退出有2种情况:
- 正常退出:main()函数调用结束;调用
exit()
函数;调用_exit()
函数 - 异常退出:向进程发射
退出信号
使进程退出
exit函数
- 功能:调用
exit()
的进程会直接退出,并且将exit()
中的参数作为子进程的退出码交给父进程,同时会刷新缓冲区中的内容 - 参数:子进程的退出码
- 返回值:空
- 注意:
exit()
是库函数而不是系统调用接口
[sy1@VM-16-11-centos 1_29]$ cat process_control.c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
int main( void )
{
printf("Process is running!");//字符串存放在缓冲区中
sleep(3);//3s后看见打印内容
exit(1);//退出码设置为1
return 0;//return前进程已经退出了,所以退出码为1而不是0
}
[sy1@VM-16-11-centos 1_29]$ make
gcc -o process_control process_control.c
[sy1@VM-16-11-centos 1_29]$ ./process_control
#3秒后显示结果
Process is running![sy1@VM-16-11-centos 1_29]$ echo $?
1
函数中调用exit()
,进程仍然会退出
[sy1@VM-16-11-centos 1_29]$ cat process_control.c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
void fun()
{
exit(1);
}
int main( void )
{
printf("Process is running!\n");//字符串存放在缓冲区中
sleep(3);//3s后看见打印内容
fun();//函数中调用exit,进程仍然会退出
return 0;//return前进程已经退出了,所以退出码为1而不是0
}
[sy1@VM-16-11-centos 1_29]$ ./process_control
Process is running![sy1@VM-16-11-centos 1_29]$ echo $?
1
_exit
- 功能:调用
_exit()
的进程会直接退出,并且将_exit()
中的参数作为子进程的退出码交给父进程,不会刷新缓冲区中的内容 - 参数:子进程的退出码
- 返回值:空
- 注意:
_exit()
是系统调用而不是库函数
_exit
不会刷新缓冲区:
[sy1@VM-16-11-centos 1_29]$ cat process_control.c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
int main( void )
{
printf("Process is running!");
sleep(3);
_exit(1);//没有刷新缓冲区就退出了,看不见打印内容
return 0;//return前进程已经退出了,所以退出码为1而不是0
}
[sy1@VM-16-11-centos 1_29]$ ./process_control
[sy1@VM-16-11-centos 1_29]$ #什么内容也看不见
exit与_exit对比
-
相同点:都会让调用的进程退出并且返回给父进程与参数值相同的退出码
-
不同点:
exit
是一个库函数,会刷新缓冲区;_exit
是一个系统调用,不会刷新缓冲区。exit
是_exit
的封装。[!tip]
- 缓冲区是存在于库中的,而不是操作系统里。缓冲区若存在OS中,系统调用为了有效地利用OS资源一定会释放缓冲区,但是
_exit
并没有刷新缓冲区,缓冲区不会被系统调用刷新,但是会被库函数刷新,说明缓冲区存在于库中。 - 之所以说C/C++具有可移植性是因为在不同平台下,它们的有基于不同平台实现的库。Linux下和Windows下的系统调用肯定不完全一样,设计者将Linux的系统调用封装到一个库中,将Windows的系统调用封装到另一个库中,在库中提供统一的函数接口。用户在不同平台下安装时只需要安装相应的库即可,无需了解不同平台的系统调用具体是怎样的。
- 缓冲区是存在于库中的,而不是操作系统里。缓冲区若存在OS中,系统调用为了有效地利用OS资源一定会释放缓冲区,但是
进程等待
进程等待的必要性
一个进程结束时不会立刻从内存中释放,该进程的PCB结构仍然在内存中用于保留该进程的退出信息,当父进程读取该进程的退出信息后,子进程的PCB结构才会从内存中释放。
父进程可以通过进程等待
的方式读取子进程的退出信息。进程等待的意义:
- 回收子进程的PCB结构,释放资源
- 获取子进程退出信息,了解子进程任务执行情况
进程等待可以让一个进程去另一个进程的PCB中读取该进程的退出信息。
进程等待的方法
wait
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int* status);
- 功能:父进程以
阻塞等待
的方式等待任意一个子进程 - 参数:输出型参数,保存子进程的退出状态,不关心退出状态可以传
NULL
- 返回值:等待成功返回子进程的pid,等待失败返回-1
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
// child
int cnt = 5;
while(cnt)
{
printf("Child is running, pid: %d, ppid: %d\n", getpid(), getppid());
sleep(1);
cnt--;
}
printf("子进程退出,马上变僵尸\n");
exit(1);
}
printf("父进程休眠\n");
sleep(10);
printf("父进程开始回收子进程\n");
pid_t rid = wait(NULL); // 阻塞等待
if(rid > 0)
{
printf("wait success, rid: %d\n", rid);
}
return 0;
}
运行结果:
子进程结束后,父进程过了5s才回收子进程,也就是子进程中间会有5s的僵尸状态!
[!Tip]
fork()之后,父子进程谁先运行由调度器决定,但是父进程要晚于子进程退出,因为父进程需要wait子进程
我们创建子进程更重要的目的是希望子进程为我们完成其他的任务,那我们当然需要知道这个任务完成的怎么样,我们可以通过输出型参数status知道子进程的执行结果。status的使用通过waitpid()
方法讲解
waitpid
- 功能:父进程以阻塞等待或轮询等待的方式等待指定的子进程
- 参数:
- pid:pid>0,表示等待指定pid的进程
pid=-1,表示等待任意一个子进程 - status:输出型参数,接受子进程的退出状态,不关心退出状态可以传
NULL
- options:等待方式。0表示阻塞等待,WNOHANG表示轮询等待,还有其它的等待方式……
- pid:pid>0,表示等待指定pid的进程
- 返回值:等待成功返回子进程pid;等待失败返回-1
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
// child
int cnt = 5;
while(cnt)
{
printf("Child is running, pid: %d, ppid: %d\n", getpid(), getppid());
sleep(1);
cnt--;
}
printf("子进程退出,马上变僵尸\n");
exit(1);//子进程退出码为1
}
printf("父进程休眠\n");
sleep(10);
printf("父进程开始回收子进程\n");
int status = 0;
pid_t rid = waitpid(id, &status, 0); // 阻塞等待,子进程执行结果保存在status中
if(rid > 0)
{
printf("wait success, rid: %d, status: %d\n", rid, status);
}
return 0;
}
运行结果:
获取进程退出状态
当wait的参数status不为NULL
时,会将指定进程的退出状态作为int值存储在status中,退出状态包括进程的 exit_code
和 exit_signal
。
status是一个int变量,该参数的的低16bit位用于存放退出信息:
- 低7位存放退出时收到的信号编号
exit_signal
- 第8位存放
core dump
- 高8位存放退出码
exit_code
在上述代码中,exit(1)将子进程的exit_code设为1,子进程退出时未收到异常信号,exit_signal为0,按照status的位图规则,status中存放的应该是 0000000100000000 ( 低 16 位 ) 0000000100000000(低16位) 0000000100000000(低16位),也就是256。
宏WIFEXITED(status)
和宏WEXITSTATUS(status)
可以直接获取进程退出状态。
WIFEXITED(status)
:查看进程是否正常退出,若进程正常退出返回真。
WEXITSTATUS(status)
:提取进程退出码。
WIFEXITED(status)的实现:
#define WIFEXITED(status) ((status)&0x7f)
WEXITSTATUS(status)的实现:
#define WEXITSTATUS(status) (((status)>>8)&0xff)
通过宏获取进程退出状态:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
// child
printf("子进程退出,马上变僵尸\n");
exit(1);
}
printf("父进程休眠\n");
sleep(1);
printf("父进程开始回收子进程\n");
int status = 0;
pid_t rid = waitpid(id, &status, 0); // 阻塞等待,子进程执行结果保存在status中
if(rid > 0)
{
if (WIFEXITED(status))
{
printf("子进程正常出\n");
printf("wait success, rid: %d, status: %d\n", rid, status);
printf("子进程退出码:%d\n", WEXITSTATUS(status));
}
}
return 0;
}
运行结果:
[sy1@VM-16-11-centos 1_29]$ ./process_control
父进程休眠
子进程退出,马上变僵尸
父进程开始回收子进程
子进程正常出
wait success, rid: 29939, status: 256
子进程退出码:1
等待方式
参数opitions表示父进程以什么方式等待子进程。常见的有:
- 阻塞等待:用0表示,子进程退出前父进程不会执行自己的代码直到子进程退出。
- 非阻塞轮询等待:用
WNOHANG
表示,父进程每隔一段时间查看子进程,子进程若没有退出父进程执行自己的代码,子进程退出后父进程读取退出信息。
当使用非阻塞等待时,waitpid的返回值有三种
- 返回值大于0:等待成功,返回子进程的pid
- 返回值等于0:成功调用函数,但是子进程没有退出。可在这种情况下编写父进程等待间隙时需要做的任务
- 返回值小于0:因为某种原因调用waitpid函数失败,errno会被设置为相应的值。
非阻塞等待通常和循环一起使用:
#define NUM 4
typedef void(*FPTR)();
FPTR task[NUM];
void printf_log()
{
printf("This is a log print!\n");
}
void printf_net()
{
printf("This is a net print!\n");
}
void printf_date()
{
printf("This is a date print!\n");
}
void init_task()
{
task[0] = printf_log;
task[1] = printf_net;
task[2] = printf_date;
task[3] = NULL;
}
void excute_task()
{
int i = 0;
for (i = 0; task[i]; i++)
{
task[i]();
}
}
int main()
{
init_task();//这里可以设置父进程等待期间执行的任务
pid_t id = fork();
if(id == 0)
{
// child
int cnt = 2;
while(cnt)
{
printf("Child is running, pid: %d, ppid: %d\n", getpid(), getppid());
sleep(1);
cnt--;
}
printf("子进程退出,马上变僵尸\n");
exit(1);//设置子进程退出码
}
int status = 0;
while (1)
{
sleep(1);
pid_t rid = waitpid(id, &status, WNOHANG); // 非阻塞等待,子进程执行结果保存在status中
if(rid > 0)
{
//等待成功
printf("wait success, rid: %d, status: %d, 子进程退出码:%d\n", rid, status, WEXITSTATUS(status));
break;//等待成功后不再等待
}
else if (rid == 0)
{
//子进程没有退出,在这里父进程可以做其他的事
printf("father say child is running, fateher can do other things \n");
excute_task();
}
else
{
//waitpid函数调用失败
perror("waitpid:");
}
}
return 0;
}
运行结果:
[sy1@VM-16-11-centos 1_29]$ ./process_control
Child is running, pid: 24565, ppid: 24564
father say child is running, fateher can do other things
This is a log print!
This is a net print!
This is a date print!
Child is running, pid: 24565, ppid: 24564
father say child is running, fateher can do other things
This is a log print!
This is a net print!
This is a date print!
子进程退出,马上变僵尸
wait success, rid: 24565, status: 256, 子进程退出码:1
进程替换
进程替换的概念及原理
创建子进程后我们希望子进程能完成特定的任务,可以通过进程替换实现。
进程替换是指OS将正在调度的进程对应的代码和数据被替换为另一个进程的代码和数据。
一个进程存在的标志有
-
PCB内核数据结构
-
虚拟内存
-
该进程对应的页表
-
硬盘中的可执行程序代码和数据被加载到物理内存
进程替换本质上是将一个进程的代码和数据加载到物理内存中覆盖原来执行进程的代码和数据,替换后进程的pid、虚拟内存、页表等其他属性不变,只改变和代码执行相关的属性。
当A进程在某个位置被B进程替换后,A未执行的部分不会再被执行。因为A进程对应的PCB结构对应的代码和数据已经变成B进程,没法找到原来A进程对应的代码和数据。
进程替换函数
进程替换本质是将磁盘中的代码和数据加载到物理内存,只有OS才能将数据从硬件拷贝到硬件上,所以进程替换底层一定需要通过系统调用接口实现,进程替换的函数有6个exec族
函数,1个系统调用
#include <unistd.h>
int execl(const char* path, char* arg, ...);
int execlp(const char* file_name, char* arg, ...);
int execle(const char* path, char* arg, ..., char* env[]);
int execv(const char* path, char* argv[]);
int execvp(const char* file_name, char* argv[]);
int execvpe(const char* file_name, char* argv[], char* env[]);
int execve(const char* path, char* argv, char* env[]);//只有这个是系统调用
返回值说明:只有替换失败返回-1,替换成功没有返回值
参数说明(A进程表示原进程,B进程表示替换进程):
- path:可执行程序名,不需要带路径,自动去
PATH
环境变量中找 - file_name:可执行程序的完整路径
- arg:可变参数字符串,表示执行B程序的方式,例如ls程序可以以
ls -a
执行,也可以ls -a -l
执行,可变参数要以NULL
结尾 - argv:一个指针数组,存放执行B程序的方式,指针数组以
NULL
结尾 - env:一个指针数组,存放B进程接收到的环境变量(没有该参数默认继承A的环境变量),指针数组以NULL结尾
[!tip]
命名的理解:
- 名字带p的表示path环境变量,不指定路径会在
PATH
环境变量中去找- 名字带l的表示以列表(list)的方式传递运行程序的方法
- 名字带v的表示以数组(vector)的方式传递运行程序的方法
- 名字带e的表示A进程会将指定的环境变量(environ)传给B进程
在终端输入命令时,本质是shell进程创建一个子进程,让子进程进程替换为命令对应的进程。shell进程负责与我们进行交互获取我们输入的命令。
exec
函数成功没有返回值。函数的返回值是给旧进程使用,而进程替换后旧进程的代码不会再执行,所以接收到的返回值没意义。创建进程时,子进程和父进程具有相同的
虚拟内存地址
,虚拟内存地址中存放了环境变量。子进程会继承父进程的环境变量。只有
execve
是系统调用,其余的是execve
的封装,它们之间的封装关系如下:
exec族
函数的使用:
#include <unistd.h>
int main()
{
char* const argv[] = {"ls", "-a", NULL};
char* const argv2[] = {"env", NULL};
char* const envp[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL}; //环境变量表需要以NULL结尾
extern char** environ;
//execl("/bin/ls", "ls", "-a", NULL);
//execlp("ls", "ls", "-a", NULL); // 带p的,可以使用环境变量PATH,无需写全路径
//execle("/bin/env", "env", NULL, envp);// 带e的,需要自己组装环境变量
//execv("/bin/ls", argv);
//execvp("ls", argv); // 带p的,可以使用环境变量PATH,无需写全路径
//execvpe("env", argv2, envp); // 带e的,需要自己组装环境变量
exit(1);
}
创建子进程,子进程执行进程替换:
//process_control.c
int main()
{
pid_t id = fork();
if (id == 0)
{
//child
char* argv[] = {"./test", "-a", "-b", NULL};
//child
execv("./test", argv);
perror("execv:");
}
pid_t rid = waitpid(id, NULL, 0);
if (rid < 0)
{
perror("waitpid:");
exit(0);
}
return 0;
}
//test.cc
#include <iostream>
using namespace std;
int main(int argn, char* argv[])
{
cout << "hello exec!\n";
for (int i = 0; argv[i]; i++)
{
cout << argv[i] << endl;
}
return 0;
}
运行结果:
[sy1@VM-16-11-centos 1_29]$ ./process_control hello exec! ./test -a -b