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 25 18:25 a.out
-rwxr-xr-x 1 pi pi 8.0K 24 14:45 echoarg
-rw-r--r-- 1 pi pi  165 24 14:44 echoarg.c
-rw-r--r-- 1 pi pi  247 24 14:56 execl1.c
-rw-r--r-- 1 pi pi  239 25 18:18 execl.c
-rw-r--r-- 1 pi pi  231 24 14:55 execlp.c
-rw-r--r-- 1 pi pi  258 24 15:04 execlvp.c
-rw-r--r-- 1 pi pi  356 21 01:23 fork1.c
-rw-r--r-- 1 pi pi  442 21 00:43 fork.c
-rw-r--r-- 1 pi pi  202 21 00:14 getpid.c
-rwxr-xr-x 1 pi pi 8.2K 21 01:40 newpro
-rw-r--r-- 1 pi pi  569 24 13:05 newpro.c
-rw-r--r-- 1 pi pi  435 25 18:25 popen.c
-rw-r--r-- 1 pi pi  710 21 02:05 vfock.c
-rw-r--r-- 1 pi pi  753 24 14:07 wait.c
-rwxr-xr-x 1 pi pi 8.2K 24 14:11 waitpid
-rw-r--r-- 1 pi pi  769 24 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

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值