Linux多进程/线程编程之【fork()和exec()】
在多进程中fork()和exec()通常是结合起来一起使用,在多线程中,通常不建议使用fork()和exec()
目录
一、fork系统调用创建子进程
1.1 为什么要创建子进程
(1)每一次程序的运行都需要一个进程,需要创建多个进程实现宏观上的并行。
1.2 fork系统调用的内部原理
(1)进程的分裂生长模式:如果操作系统需要一个新进程来运行一个程序,那么操作系统就会用一个现有的进程来复制生成一个新进程,老进程叫父进程,新进程叫子进程。
(2)fork系统调用调用一次会返回两次,使用fork后要用if判断返回值,返回值等于0的就是子进程,返回值大于0的就是父进程。
(3)fork的返回值在父进程中等于本次创建的子进程的进程ID,在子进程中等于0;
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main(void)
{
pid_t p1 = -1;
p1 = fork(); // 返回2次
if (p1 == 0)
{
// 这里一定是子进程
// 先sleep一下让父进程先运行,先死
sleep(1);
printf("子进程, pid = %d.\n", getpid());
printf("hello world.\n");
printf("子进程, 父进程ID = %d.\n", getppid());
}
if (p1 > 0)
{
// 这里一定是父进程
printf("父进程, pid = %d.\n", getpid());
printf("父进程, p1 = %d.\n", p1);
}
if (p1 < 0)
{
// 这里一定是fork出错了
}
// 在这里所做的操作会被执行两遍
//printf("hello world, pid = %d.\n", getpid());
return 0;
}
1.3 关于子进程
子进程由父进程复制而来,有自己独立的PCB,被内核同等调度。
1.4 线程和fork
当多线程进程调用fork()时,仅会将发起调用的线程复制到子进程中。子进程中该线程的线程ID与父进程中发起fork()调用线程的线程ID相一致(后面会有例子说明)。其他线程均在子进程中消失,也不会为这些线程调用清理函数以及针对线程特有数据的解构函数。
这将导致如下一些问题:(线程中fork()会导致子进程的内存泄漏)
虽然只将发起调用的线程复制到子进程中,但全局变量的状态以及所有的Pthreads对象(如互斥量、条件变量等)都会在子进程中得以保留。(因为在父进程中为这些Pthreads对象分配了内存,而子进程则获得了该内存的一份拷贝。)这会导致很棘手的问题。例如,假设在调用fork()时,另一线程已然锁定了某一互斥量,且对某一全局数据结构的更新也做到了一半。此时,子进程中的该线程无法解锁这一互斥量(因为其并非该互斥量的属主),如果试图获取这一互斥量,线程会遭阻塞。此外,子进程中的全局数据结构拷贝可能也处于不一致状态,因为对其进行更新的线程在执行到一半时消失了。
因为并未执行清理函数和针对线程特有数据的解构函数,多线程程序的fork()调用会导致子进程的内存泄露。另外,子进程的线程很可能无法访问(父进程中)由其他线程所创建的线程特有数据项,因为(子进程)没有相应的引用指针。
二、exec族函数及实战
需要注意的是,exec并不是一个函数,其实只是一组函数的统称,包括六个函数:
2.1 为什么需要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[]);
int execve(const char *path, char *const argv[], char *const envp[]);
(1)fork系统调用是为了执行新的程序,但需要直接在子进程的if里写入新的程序代码,这样不够灵活,譬如说我们如果想要执行ls -al 就不可以了(没有源代码,只有可执行程序)。
(2)使用exec族函数可以运行新的可执行程序,即把一个编译好的可执行程序直接加载运行。
(3)一个进程一旦调用exec类函数,它本身就"死亡"了,系统把代码段替换成新的程序的代码,废弃原有的数据段和堆栈段,并为新程序分配新的数据段与堆栈段,唯一留下的,就是进程号,也就是说,对系统而言,还是同一个进程,不过已经是另一个程序了。(不过exec类函数中有的还允许继承环境变量之类的信息。) 那么如果我的程序想启动另一程序的执行但自己仍想继续运行的话,怎么办呢?那就是结合fork与exec的使用
2.2 exec族的6个函数介绍
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[]);
(1)execl和execv:
(l后缀:命令参数部分必须以","相隔,且最后一个参数必须为NULL)
(v后缀: 命令参数部分必须是1个以NULL结尾的字符串指针数组的头部指针)
这两个是最基本的exec,都可以用来加载运行可执行程序,区别是传参格式不同。
execl是把多个参数(多个字符串,必须以NULL结尾)依次排列(l就是list的缩写);
而execv是把多个参数事先放入一个字符串数组NULL结尾),再把这个字符串数组传给execv函数。
(2)execlp和execvp:(p后缀,参数执行文件部分可以不带路径,exec函数会去$PATH寻找)
这两个函数较上面两个来说,上面两个函数必须指定可执行程序的全路径,而这两个加了p的函数可以只指定file即文件名,函数会到环境变量PATH所指定的目录下去找。
(3)execle和execvpe:(e后缀,参数必须带环境变量部分)
main函数的原型其实不止是int main(int argc, char argv),还可以是int main(int argc, argv, char **env),第三个参数是一个字符串数组,内容是环境变量。
如果用户在执行execle和execvpe的时候没有传递第三个参数,则程序会默认从父进程那里继承一份环境变量(默认的,最早来源于OS);如果传递了第三个参数则替代了默认的环境变量。
2.3 exec()函数返回
exec函数会取代执行它的进程, 也就是说, 一旦exec函数执行成功, 它就不会返回了, 进程结束
与一般情况不同,exec函数族的函数执行成功后不会返回,因为调用进程的实体,包括代码段,数据段和堆栈等都已经被新的内容取代,只留下进程ID等一些表面上的信息仍保持原样,颇有些神似"三十六计"中的"金蝉脱壳"。看上去还是旧的躯壳,却已经注入了新的灵魂。只有调用失败了,它们才会返回一个-1,从原程序的调用点接着往下执行。
通常exec会放在fork() 函数的子进程部分, 来替代子进程执行啦, 执行成功后子程序就会消失, 但是执行失败的话, 必须用exit()函数来让子进程退出!
2.4 exec编程实战
-
exec实战1
(1)使用execl运行ls -l -a
execl("/bin/ls", "ls", "-l", "-a", NULL);
(2)使用execv运行ls
char * const arg[] = {"ls", "-l", "-a", NULL};
execv("/bin/ls", arg);
(3)使用execl运行自己写的程序
execl("hello", "aaa", "bbb", NULL);
(4)使用execv运行自己写的程序
char * const arg[] = {"aaa", "bbb", NULL};
execv("hello", arg);
-
exec实战2
(1)execlp
execlp("ls", "ls", "-l", "-a", NULL);
(2)execle
char * const envp[] = {"AA=aaaa", "XX=abcd", NULL};
execle("hello", "hello", "-l", "-a", NULL, envp);
2.5 线程和exec()
只要有任一线程调用了exec()系列函数之一时,调用程序将被完全替换。除了调用exec()的线程之外,其他所有线程都将立即消失。没有任何线程会针对线程特有数据执行结构函数,也不会调用清理函数。该进程的所有互斥量(为进程私有)和属于进程的条件变量都会消失。调用exec()之后,调用线程的线程ID是不确定的。
三、总结
3.1 多进程
在多进程中fork()和exec()通常是结合起来一起使用:
fork()创建一个子进程,且fork()后立即接上exec()函数,在fork()创建的子进程中,exec函数会取代执行相关脚本文件,执行程序,然后退出,不会返回
3.2 多线程
在多线程中,通常不建议使用fork()和exec():
fork()函数的调用会导致在子进程中除调用线程外的其它线程全都终止执行并消失,因此在多线程的情况下会导致死锁和内存泄露的情况。在进行多线程编程的时候尽量避免fork()的调用,同时在程序在进入main函数之前应避免创建线程,因为这会影响到全局对象的安全初始化。线程不应该被强行终止,因为这样它就没有机会调用清理函数来做相应的操作,同时也就没有机会来释放已被锁住的锁,如果另一线程对未被解锁的锁进行加锁,那么将会立即发生死锁,从而导致程序无法正常运行。
更多推荐


所有评论(0)