Linux进程基础知识(fork、vfork、exec族函数、system、popen
前言
1、进程拿来干嘛?创建多个进程是任务分解时行之有效的方法。如,网络服务器进程可在监听客户端请求的同时,为处理每个请求而创立一个新的进程,同时,服务器进程会继续监听更多的客户端连接请求。
2、进程与程序的区别?
进程是一个可执行程序的实例。如a.out;
程序是包含了一系列信息的文件,这些信息描述了如何运行时创建一个进程。
3、如何查看系统中的进程?
ps -aux | grep a.out:在当前运行的所有进程中查找a.out进程。
4、C程序的内存空间怎么分配?
命令行参数和环境变量,如argc、argv;
栈:自动变量以及每次函数调用时所需保存的信息存放在此段中。每次函数调用时,其返回地址以及调用者的环境信息都存放在栈中。然后,最近被调用的函数在栈上为其自动和临时变量分配存储空间。;
堆:用于进行动态分配内存,如malloc();
正文段:由CPU执行的机器指令组成。通常,正文段是可共享的,在存储器中也只有一个副本,另外,正文段通常是只读的,防止程序由于意外修改其指令。
初始化数据段:通常将此段称为数据段,它包含了程序中需要明确赋初值的变量。
未初始化数据段:在程序开始执行之前,内核将此段中的数据初始化为0或空指针。
一、进程的创建
1、fork()
函数原型:
SYNOPSIS:
#include <unistd.h>
pid_t fork(void);
描述:创建新进程,fork()调用成功,返回2次。返回0,代表当前进程为子进程。返回值大于0,代表当前进程是父进程。返回-1,调用失败。
调用后存在两个进程,每个进程都会从fork()的返回值处继续执行,调用之后,哪个进程先执行是无法确定的,由CPU的进程调度决定。
子进程的内存是对父进程的写时拷贝(Write on copy)。
探究fork的执行情况:
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
int main()
{
pid_t pid;
pid_t pid2;
pid = getpid();
printf("before pid is %d\n",getpid()); //1
fork();
pid2 = getpid();
printf("after pid is %d\n",getpid()); //2
if(pid == pid2){
printf("this is father's pid print,pid is %d\n",getpid()); //3
}
else{
printf("this is child's pid print,pid is %d\n",getpid()); //4
}
return 0;
}
运行结果为:先打印1、2、3.再打印2、4,且两个输出的进程ID不一样。
探究fork的返回值情况:
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
int main()
{
pid_t pid;
printf("father pid is %d\n",getpid());
pid = fork();
if(pid > 0){
printf("this is father's pid print,pid is %d\n",getpid());
}
else if(pid == 0){
printf("this is child's pid print,pid is %d\n",getpid());
}
return 0;
}
2、vfork()
函数原型:
SYNOPSIS:
#include <sys/types.h>
#include <unistd.h>pid_t vfork(void);
vfork()与fork()的关键区别:
1、子进程直接使用父进程的存储空间,不拷贝。
2、vfork()保证,子进程先执行,等子进程调用_exit()或exec()后,将暂停执行,父进程才执行。
探究vfork()的父子进程执行情况:
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
int main()
{
int cnt = 0;
pid_t pid;
pid = vfork();
if(pid > 0){
while(1){
printf("this is father's pid print,pid is %d\n",getpid());
sleep(1);
cnt ++;
if(cnt == 6){
exit(0);
}
}
}
else if(pid == 0){
while(1){
printf("this is child's pid print,pid is %d\n",getpid());
sleep(1);
cnt ++;
if(cnt == 3){
exit(0);
}
}
}
return 0;
}
结果表示:先执行子进程3次,再执行父进程3次。即说明父子共享内存,和先执行子进程,后父进程。
3、模拟服务器—客户端
(不会网络编程QAQ)
模拟这一功能:网络服务器进程可在监听客户端请求的同时,为处理每个请求而创立一个新的进程,同时,服务器进程会继续监听更多的客户端连接请求。
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
int main()
{
pid_t pid;
int data = 10;
while(1){
printf("please input a data:\n");
scanf("%d",&data);
if(data == 1 || data == 6){
pid = fork();
if(pid > 0){
}
else if(pid == 0){ //子进程处理响应
while(1){
printf("do net request,pid = %d\n",getpid());
sleep(3); //程序休眠3s防止刷屏
}
}
}
else{
printf("wait,do nothing.\n");
}
}
return 0;
}
二、exec族函数(execl、execlp、execvp)
exec族函数函数的作用:
我们用fork函数创建新进程后,经常会在新进程中调用exec函数去执行另外一个程序。当进程调用exec函数时,该进程被完全替换为新程序。因为调用exec函数并不创建新进程,所以前后进程的ID并没有改变。
功能:
在调用进程内部执行一个可执行文件。可执行文件既可以是二进制文件,也可以是任何Linux下可执行的脚本文件。
函数族:
exec函数族分别是:execl, execlp, execle, execv, execvp, execvpe
函数原型:
#include <unistd.h>
extern char **environ;
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 execvpe(const char *file, char *const argv[],char *const envp[]);
返回值:
exec函数族的函数执行成功后不会返回,调用失败时,会设置errno并返回-1,然后从原程序的调用点接着往下执行。
参数说明:
path:可执行文件的路径名字
arg:可执行程序所带的参数,第一个参数为可执行文件名字,没有带路径且arg必须以NULL结束
file:如果参数file中包含/,则就将其视为路径名,否则就按 PATH环境变量,在它所指定的各目录中搜寻可执行文件。
exec族函数参数极难记忆和分辨,函数名中的字符会给我们一些帮助:
l : 使用参数列表
p:使用文件名,并从PATH环境进行寻找可执行文件
v:应先构造一个指向各参数的指针数组,然后将该数组的地址作为这些函数的参数。
1、execl()
以execl函数为例子来说明:
//echoarg.c
#include <stdio.h>
int main(int argc,char *argv[])
{
int i = 0;
for(i = 0; i < argc; i++){
printf("argv[%d]:%s\n",i,argv[i]);
}
return 0;
}
//execl.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
//int execl(const char *path, const char *arg, ...);
int main()
{
printf("before execl\n");
if(execl("./echoarg","abc","123",NULL) == -1){ //执行当前目录的可执行文件echoarg,第二、三是参数,第四个是要以NULL结尾
printf("execl failed\n");
perror("why"); //perror()打印调用失败原因
}
printf("after execl\n");
return 0;
}
终端执行:
$ ./a.out
before execl
argv[0]:abc
argv[1]:123
文件echoarg的作用是打印命令行参数。然后再编译execl.c并执行execl可执行文件。用execl 找到并执行echoarg,将当前进程main替换掉,所以”after execl” 没有在终端被打印出来。
也可以这样做:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
printf("This pro get system date:\n");
if(execl("/bin/date","date",NULL) == -1){ //打印时间
printf("execl failed\n");
}
printf("after execl\n");
return 0;
}
$ gcc execl.c
$ ./a.out
This pro get system date:
Sat Feb 6 19:01:56 CST 2021
命令data也是一个可执行文件,在/bin/date目录下,即也可以使用exec族函数进行进程跳转。
2、execlp()
execlp()和execl()的区别是,execlp()从PATH环境变量中进行寻找可执行文件。
以execlp()为例子来说明:
//execlp.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
//int execlp(const char *file, const char *arg, ...);
int main()
{
printf("before execl\n");
if(execlp("ls","ls","-l",NULL) == -1){
printf("execl failed\n");
}
printf("after execl\n");
return 0;
}
终端中执行./execlp,在终端打印"before execl",后打印当前目录文件的详细信息。
3、execvp()
v指的是 应先构造一个指向各参数的指针数组,然后将该数组的地址作为这些函数的参数。
p指的是 从PATH环境变量中进行寻找可执行文件
//execvp.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
//int execvp(const char *file, char *const argv[]);
int main()
{
printf("before execl\n");
char *argv[] = {"ls","-l",NULL};
if(execvp("ls",argv) == -1){
printf("execl failed\n");
}
printf("after execl\n");
return 0;
}
执行结果与execlp.c一样。
exec族函数参考于:https://blog.csdn.net/u014530704/article/details/73848573
三、system()函数
函数原型:
NAME:
system - execute a shell command
SYNOPSIS
#include <stdlib.h>
int system(const char *command);
Linux版system函数的源码:
#include
#include
#include
#include
int system(const char * cmdstring)
{
pid_t pid;
int status;
if(cmdstring == NULL){//system接受的命令为NULL时直接返回
return (1);
}
if((pid = fork())<0){ //创建子进程失败
status = -1;
}
else if(pid == 0){ //子进程则是调用execl来启动一个程序代替自己
execl("/bin/sh", "sh", "-c", cmdstring, (char *)0); //调用shell,这个shell的路径是/bin/sh
//sh -c a.out <--->./a.out
-exit(127); //子进程正常执行则不会执行此语句
}
else{ //父进程使用waitpid等待子进程结束
while(waitpid(pid, &status, 0) < 0){
if(errno != EINTER){
status = -1;
break;
}
}
}
return status;
}
返回值:成功,则返回进程的状态值;当sh不能执行时,返回127;失败返回-1。
例子程序:
//system.c
//终端打印时间
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
printf("This pro get system date:\n");
if(system("date") == -1){
printf("execl failed\n");
}
printf("after execl\n");
return 0;
}
$ gcc system.c
$ ./a.out
This pro get system date:
Sat Feb 6 19:01:56 CST 2021
after execl
从运行结果来看,它的功能基本代替execl(),比execl()使用起来也简单。但与execl()不同的是system()会回来本进程,execl()跳转另一个进程就不会回来。
四、popen()函数
函数原型:
#include “stdio.h”
FILE popen( const char* command, const char* mode )
函数作用:
Linux中的popen()函数可以在程序中执行一个shell命令,并返回命令执行的结果。有两种操作模式,分别为读和写。在读模式中,程序中可以读取到命令的输出,其中有一个应用就是获取网络接口的参数。在写模式中,最常用的是创建一个新的文件或开启其他服务等。
参数说明:
command: 是一个指向以 NULL 结束的 shell 命令字符串的指针。这行命令将被传到 bin/sh 并使用 -c 标志,shell 将执行这个命令。
mode: 只能是读或者写中的一种,得到的返回值(标准 I/O 流)也具有和 type 相应的只读或只写类型。如果 type 是 “r” 则文件指针连接到 command 的标准输出;如果 type 是 “w” 则文件指针连接到 command 的标准输入。
返回值:
如果调用成功,则返回一个读或者打开文件的指针,如果失败,返回NULL,具体错误要根据errno判断。
比system()好处:
popen()可以获取运行的输出结果。
例子说明:
//popen.c
//把
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
//FILE *popen(const char *command, const char *type);
//size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
//size_t fwrite(const void *ptr, size_t size, size_t nmemb,FILE *stream);
int main()
{
char ret[1024] = {0};
FILE *fp;
FILE *fd;
fp = popen("ls -lh","r"); //执行shell命令,读的方式
if(fp == NULL){
return -1;
}
int nread = fread(ret,1,1024,fp); //popen()读到的,每次写1次,写1024次,写道ret数组中
printf("read to ret %d byte,ret %s\n",nread,ret);
fd = fopen("./ls-lh.txt","w"); //创建文件
fwrite(ret,strlen(ret),1,fd); //ret里面的数据,写1次,写ret的这么长,写道fd中
pclose(fp);
fclose(fd);
return 0;
}
执行结果:
$ gcc popen.c
$ ./a.out
read to ret 818 byte,ret 总用量 96K
-rwxr-xr-x 1 pi pi 8.2K 2月 5 18:25 a.out
-rwxr-xr-x 1 pi pi 8.0K 2月 4 14:45 echoarg
-rw-r--r-- 1 pi pi 165 2月 4 14:44 echoarg.c
-rw-r--r-- 1 pi pi 247 2月 4 14:56 execl1.c
-rw-r--r-- 1 pi pi 239 2月 5 18:18 execl.c
-rw-r--r-- 1 pi pi 231 2月 4 14:55 execlp.c
-rw-r--r-- 1 pi pi 258 2月 4 15:04 execlvp.c
-rw-r--r-- 1 pi pi 356 2月 1 01:23 fork1.c
-rw-r--r-- 1 pi pi 442 2月 1 00:43 fork.c
-rw-r--r-- 1 pi pi 202 2月 1 00:14 getpid.c
-rwxr-xr-x 1 pi pi 8.2K 2月 1 01:40 newpro
-rw-r--r-- 1 pi pi 569 2月 4 13:05 newpro.c
-rw-r--r-- 1 pi pi 435 2月 5 18:25 popen.c
-rw-r--r-- 1 pi pi 710 2月 1 02:05 vfock.c
-rw-r--r-- 1 pi pi 753 2月 4 14:07 wait.c
-rwxr-xr-x 1 pi pi 8.2K 2月 4 14:11 waitpid
-rw-r--r-- 1 pi pi 769 2月 4 14:12 waitpid.c
ls-lh.txt:
总用量 96K
-rwxr-xr-x 1 pi pi 8.3K 2月 5 18:32 a.out
-rwxr-xr-x 1 pi pi 8.0K 2月 4 14:45 echoarg
-rw-r–r-- 1 pi pi 165 2月 4 14:44 echoarg.c
-rw-r–r-- 1 pi pi 247 2月 4 14:56 execl1.c
-rw-r–r-- 1 pi pi 239 2月 5 18:18 execl.c
-rw-r–r-- 1 pi pi 231 2月 4 14:55 execlp.c
-rw-r–r-- 1 pi pi 258 2月 4 15:04 execlvp.c
-rw-r–r-- 1 pi pi 356 2月 1 01:23 fork1.c
-rw-r–r-- 1 pi pi 442 2月 1 00:43 fork.c
-rw-r–r-- 1 pi pi 202 2月 1 00:14 getpid.c
-rwxr-xr-x 1 pi pi 8.2K 2月 1 01:40 newpro
-rw-r–r-- 1 pi pi 569 2月 4 13:05 newpro.c
-rw-r–r-- 1 pi pi 634 2月 5 18:32 popen.c
-rw-r–r-- 1 pi pi 710 2月 1 02:05 vfock.c
-rw-r–r-- 1 pi pi 753 2月 4 14:07 wait.c
-rwxr-xr-x 1 pi pi 8.2K 2月 4 14:11 waitpid
-rw-r–r-- 1 pi pi 769 2月 4 14:12 waitpid.c