【Linux C | 进程】创建进程 | vfork函数+exec函数,以及system函数——文中很多C语言例子帮助理解

发布时间:2024年01月23日

😁博客主页😁:🚀https://blog.csdn.net/wkd_007🚀
🤑博客内容🤑:🍭嵌入式开发、Linux、C语言、C++、数据结构、音视频🍭
🤣本文内容🤣:🍭介绍 🍭
😎金句分享😎:🍭你不能选择最好的,但最好的会来选择你——泰戈尔🍭

本文未经允许,不得转发!!!


在这里插入图片描述

🎄一、vfork 函数概述

?1.1 vfork 函数介绍

函数原型

#include <unistd.h>
pid_t vfork(void);

返回值:父进程返回子进程ID,子进程返回0,出错在父进程返回-1。


vfork 和 fork 函数很像,也是用于创建子进程的,但与fork函数有两点不同:

  • 1、vfork 创建的子进程不复制父进程的任何资源,而是直接占用父进程的资源运行代码。子进程不能从当前函数return或调用exit(),但可以调用_exit()
  • 2、调用 vfork 后,父进程会阻塞,直到子进程终止或调用execcv系列函数。

下表是fork函数和vfork函数的对比:

区别forkvfork
执行两次fork函数之后的代码会执行两次,父进程执行一次,子进程执行一次vfork函数之后的代码会执行两次,父进程执行一次,子进程执行一次
返回两次fork函数返回两次,在父进程返回子进程的进程ID,在子进程返回 0vfork函数返回两次,在父进程返回子进程的进程ID,在子进程返回 0
复制资源fork函数创建的子进程会复制父进程除了代码区之外所有区域(包括数据段、bss段、堆、栈、文件描述符等)vfork 不负责父进程资源
谁先执行调用fork函数后,无法确定是子进程先执行,还是父进程先执行vfork之后,子进程先执行,父进程阻塞直到子进程终止或调用exec系列函数

?1.2 vfork 函数举例

看例子:
1、即使在子进程sleep(3),子进程依旧先运行,父进程一直阻塞到子进程终止;
2、在子进程中修改变量后,父进程打印的变量全是子进程修改的,说明它们共享资源。

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>

int i;		// 存在于程序的bss段
int j=100;	// 存在于程序的数据段
int main()
{
	short s=10; // 存在于栈
	char *str = malloc(20); // 存在于堆
	strcpy(str, "abcdef");
	
    printf("程序开始执行!\n");
	
    pid_t pid = vfork();
	if(pid>0) // 父进程
	{
		printf("父进程执行过程中.... fatherPid=%d, childPid=%d\n", getpid(), pid);
		printf("&i=%p, &j=%p, &s=%p str=%p\n", &i,&j,&s,str);
		printf("i=%d, j=%d, s=%d str=[%s]\n", i,j,s,str);
		i=1;
		j=2;
		s=3;
	}
	else if(pid==0) // 子进程
	{
		sleep(3);
		printf("子进程执行过程中.... fatherPid=%d, childPid=%d\n", getppid(), getpid());
		printf("&i=%p, &j=%p, &s=%p str=%p\n", &i,&j,&s,str);
		printf("i=%d, j=%d, s=%d str=[%s]\n", i,j,s,str);
		
		i=1;
		j=2;
		s=3;
		strcpy(str, "ABCDEF");
		printf("i=%d, j=%d, s=%d str=[%s]\n", i,j,s,str);
		//return(0);  // 执行报错
		//exit(0);
		_exit(0);
	}
	else
	{
		printf("fork error\n");
	}
	free(str);
    while(1)
		sleep(1);
    printf("程序执行结束\n");
    return 0;
}

执行结果:

最后,说明一下,vfork函数一般是结合exec系列函数一起使用的。那什么是exec系列函数呢?


在这里插入图片描述

🎄二、exec 函数

exec系列函数的主要作用:替代原有的进程的代码段、数据段、BSS段、堆区和栈区,然后从新的main开始执行,但是保留了原子进程的PID。

替换的意思就是,调用进程成功执行完exec函数后,原来进程在exec后面的代码都不会被执行了

exec函数原型如下:

#include <unistd.h>
int execl(const char *path, const char *arg, ...);
int execv(const char *path, char *const argv[]);
int execlp(const char *file, const char *arg, ...);
int execvp(const char *file, char *const argv[]);
int execle(const char *path, const char *arg, ..., char * const envp[]);
int execve(const char *path, char *const argv[], char *const envp[]);

这6个函数中,只有execve是系统调用,另外5个是库函数,它们最终都会调用execve

这6个函数怎么记忆、使用:

  • 函数名前4个都是exec
  • 第五个字母:
    • l:是list的意思,要求新程序的每个命令行参数都作为单独的一个函数参数来传递;
      execl("/bin/ls", "ls", "-l", "-a", "./", NULL);
      
    • v:是vector的意思,要求先构造一个指向各个参数的指针数组,再将该数组作为参数传递给execv*
      char *args[]={"ls", "-l", "-a", "./", NULL};
      execv("/bin/ls",args);
      
  • 第六个字母:
    • p:是path的意思,如果参数file包含/,将将其视为路径名;否则从PATH环境变量查找可执行文件;
      execlp("ls", "ls", "-l", "-a", "./", NULL);
      char *args[]={"ls", "-l", "-a", "./", NULL};
      execvp("ls",args);
      
    • e:是environ的意思,可以给新程序传递一个指向环境字符串指针数组的指针。
      char *env_init[] = {"USER=wkd_007", "HOME=/home/wkd", NULL};
      execle("./my_echo", "my_echo", "-l", "-a", "./", NULL, env_init);
      
      char *args[]={"my_echo", "-l", "-a", "./", NULL};
      execve("./my_echo", args, env_init);
      

注意: 使用时,第0个参数也需要传递给exec函数。例如命令ls -l -a ./,则ls是第0个参数。


?2.1 execl、execv 函数举例

#include <stdio.h>
#include <unistd.h>

#define EXECL_TEST 1
int main()
{
	int i = 10;
#if EXECL_TEST
	printf("EXECL_TEST \n");
    //int res = execl("/bin/ls", "123", "-l", "-a", "./", NULL); // 第0个参数需要传,但exec函数没有使用。
	int res = execl("/bin/ls", "ls", "-l", "-a", "./", NULL);
	if(res == -1)
    {
        perror("execl");
    }
#else
	printf("EXECV_TEST \n");
	char *args[]={"ls", "-l", "-a", "./", NULL};
    int res = execv("/bin/ls",args);
	if(res == -1)
    {
        perror("execl");
    }
#endif
	printf("i=%d\n",i);  // 不会打印,因为exec函数执行成功后,会替换代码段
    
	return 0;
}

?2.2 execlp、execvp 函数举例

#include <stdio.h>
#include <unistd.h>

#define EXECLP_TEST 0
int main()
{
	int i = 10;
#if EXECLP_TEST
	int res = execl("ls", "ls", "-l", "-a", "./", NULL);
	if(res == -1)
    {
        perror("execl"); // 报错:execl: No such file or directory
    }
	res = execlp("ls", "ls", "-l", "-a", "./", NULL);
	if(res == -1)
    {
        perror("execlp");
    }
#else
	char *args[]={"ls", "-l", "-a", "./", NULL};
    int res = execv("ls",args);
	if(res == -1)
    {
        perror("execv"); // 报错:execv: No such file or directory
    }
	res = execvp("ls",args);
	if(res == -1)
    {
        perror("execvp");
    }
#endif
	printf("i=%d\n",i);  // 不会打印,因为exec函数执行成功后,会替换代码段
    
	return 0;
}

?2.3 execle、execve 函数举例

程序一:my_echo,用来打印参数列表和环境表。

// gcc my_echo.c -o my_echo
#include <stdio.h>
#include <unistd.h>
extern char** environ;
int main(int argc, char *argv[])
{
	int i=0;
	for(i=0; i<argc; i++)
	{
		printf("argv[%d] = %s\n",i, argv[i]);
	}
	
	for (i = 0; *(environ+i)!=NULL; i++)
	{
		/* echo all command-line args */
		printf ( "environ[%02d]: %s \n", i, *(environ+i) );
	}
    
	return 0;
}

程序二:

#include <stdio.h>
#include <unistd.h>
char *env_init[] = {"USER=wkd_007", "HOME=/home/wkd", NULL};
#define EXECLE_TEST 1
int main()
{
	int i = 10;
#if EXECLE_TEST
	printf("EXECLE_TEST:\n");
	int res = execle("./my_echo", "my_echo", "-l", "-a", "./", NULL, env_init);
	if(res == -1)
    {
        perror("execle");
    }
#else
	printf("EXECVE_TEST:\n");
	char *args[]={"my_echo", "-l", "-a", "./", NULL};
    int res = execve("./my_echo", args, env_init);
	if(res == -1)
    {
        perror("execve");
    }
#endif
	printf("i=%d\n",i);  // 不会打印,因为exec函数执行成功后,会替换代码段
    
	return 0;
}

在这里插入图片描述

🎄三、vfork + exec 举例

int my_system(const char * cmdstring) 
{ 
	pid_t pid; 
	int status; 
	if(cmdstring == NULL) 
	{ 
		return (1); //如果cmdstring为空,返回非零值,一般为1 
	} 
	if((pid = vfork())<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被信号中断,则返回-1 
				break; 
			} 
		} 
	} 
	return status; //如果waitpid成功,则返回子进程的返回状态 
}

在这里插入图片描述

🎄四、system 函数

函数原型:

#include <stdlib.h>
int system(const char *command);

system函数通过调用/bin/sh -c命令执行command指定的命令,并在命令完成后返回到当前进程。在执行命令期间,SIGCHLD将被阻止,SIGINT和SIGQUIT将被忽略。

因为system在其实现中调用了fork、exec和waitpid,因此有三种返回值:
1、如果fork失败,或者waitpid返回EINTR之外的错误,则system返回-1,且errno中设置了错误类型值;
2、如果exec失败(表示不能执行shell),则其返回值如同shell执行了exit(127)一样。
3、否则所有三个函数(fork、exec和waitpid)都执行成功,并且system的返回值是shell的终止状态,其格式已在waitpid中说明。

使用system而不是直接使用fork和exec的优点是:system进行了所需的各种出错处理,以及各种信号处理。但system执行效率低于exec。

例子:

#include <stdio.h>
#include <stdlib.h>

int main()
{
	int res = system("ls -l -a ./");
	if(res == -1)
    {
        perror("system");
    }
	
	return 0;
}

在这里插入图片描述

🎄五、总结

本文先举例介绍了vfork函数,再举例介绍exec系列函数,最后介绍了system函数,文中提供了很多C语言例子帮助理解。

在这里插入图片描述
如果文章有帮助的话,点赞👍、收藏?,支持一波,谢谢 😁😁😁

文章来源:https://blog.csdn.net/wkd_007/article/details/135770213
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。