进程控制(Linux)

进程控制

  • 一、进程创建
    • 1. 再识fork
    • 2. 写时拷贝
  • 二、进程终止
    • 前言——查看进程退出码
    • 1. 退出情况
      • 正常运行,结果不正确
      • 异常退出
    • 2. 退出码
      • strerror和errno
      • 系统中设置的错误码信息
      • perror
      • 异常信息
    • 3. 退出方法
      • exit和_exit
  • 三、进程等待
    • 1. 解决等待的三个问题
    • 2. 系统调用
      • wait
        • 参数为NULL
        • 使用status参数
        • 小结
      • waitpid
    • 3. 阻塞和非阻塞等待
    • 4. 进程等待的原理
  • 四、进程替换
    • 1. 概念
    • 2. exec函数族
      • ①execl
      • ②execlp
      • ③execle
      • ④execv
      • ⑤execvp
      • ⑥execvpe
      • 小结
    • 3. 系统调用——execve
    • 4. 总结

一、进程创建

1. 再识fork

在初始进程这篇博客中,浅谈了fork这个函数,在进程地址空间这篇中,也解释了一些关于fork返回值的问题。在这里再认识一下fork函数。

fork函数: 在Linux中,fork可以从已存在的进程中创建一个新的进程,新进程为子进程,原进程为父进程。

#include <unistd.h>
pid_t fork(void);
//返回值:如果成功,给父进程返回子进程PID,给子进程返回0,如果失败返回-1

来一段代码测试:

#include <stdio.h>    
#include <unistd.h>    
#include <sys/types.h>    
#include <stdlib.h>    
    
int main()    
{    
    printf("Before:pid id %d\n", getpid());    
    pid_t pid = fork();    
    
    if(pid < 0)                                                                                                                                                                                                                             
    {    
        perror("fork");    
        exit(-1);    
    }    
    printf("After:pid is %d, fork return %d\n", getpid(), pid);    
    sleep(1);    
    return 0;    
} 

上述代码运行结果:
结果

fork运行的逻辑结构:
逻辑结构

结论: fork之前,父进程独立执行。fork之后,父子进程两个执行流分别执行。那个进程先执行由调度器决定

fork常规用法:

  1. 父进程希望复制自己,使父子进程同时执行不同的代码段。eg:父进程等待客户端请求,生成子进程处理请求。
  2. 一个进程要执行一个不同的程序。(进程替换,后面讲)

2. 写时拷贝

在进程地址空间这篇博客中也对写时拷贝进行了说明

OS为了提高效率,在创建子进程时会使用写时拷贝。本质就是按需申请,不会浪费系统资源

通过触发页表的可读权限,后续判断写入错误,发生写时拷贝,并更改权限

二、进程终止

前言——查看进程退出码

程序在执行完,无论正不正确都有一个退出码,可以查看

查看方式:echo $? 这个命令的意思就是查看 ? 的内容。
注:

  1. 查看最近一个进程或是命令运行结束的退出码,退出码保存在 ? 中。
  2. 先看程序是否异常,异常结束的退出码无意义

先实践一下:

#include <stdio.h>    
    
int main()    
{    
    printf("hello Linux!\n");                                                                                                                             
    return 0;    
}

查看退出码:
查看退出码
退出码是0,程序正常运行结束

1. 退出情况

  1. 正常运行,结果正确
  2. 正常运行,结果不正确
  3. 代码异常终止

第一种情况就不再实验

正常运行,结果不正确

先看程序是否异常,如果没有异常再看退出码。

#include <stdio.h>    
#include <stdlib.h>    
#include <errno.h>    
#include <string.h>    
    
int main()    
{    
 int *p = (int*)malloc(1000*1000*1000*4);    
    if(p == NULL)    
    {    
        printf("error msg:%s\n", strerror(errno));                                                                                                      
        exit(errno);    
    }    
    return 0;    
} 

查看退出码:
查看退出码
退出码是12,可见结果不正确。我们也打印了错误信息Cannot allocate memory

异常退出

退出码无意义。
进程出现异常,本质是我们的进程收到对应的信号

#include <stdio.h>     
 
int main()    
{    
    int a = 1, b = 0;    
    a = a / b;                                                                                                                                                                                                                              
    return 0;    
}  

查看运行结果:
运行结果
注: 第二次执行echo $? 发现结果是0。原因是上一次echo $?也是程序,并且是正常运行得到正常结果

在前面也说程序没有执行完就结束,是因为OS对进程发送了异常的信号。
测试一下:

#include <stdio.h>    
#include <sys/types.h>    
#include <unistd.h>    
    
int main()    
{    
    while(1)    
    {    
        printf("PID: %d\n", getpid());    
        sleep(1);                                                                                                                                                                                                                           
    }    
    return 0;    
} 

测试结果:
测试结果
对3330进程发送了11号信号,所以左边的会话弹出段错误,并且程序结束。

2. 退出码

父进程和用户可能会关心这个进程的运行结果,如果出错,那就要了解原因,而退出码对应着错误原因。退出码是程序执行结果和错误类型的一种有效表示方式。

C语言提供了两个函数和一个全局变量显示程序的错误信息。

strerror和errno

  1. strerror:查看系统对应的错误码信息
  2. errno:全局变量,strerror想要查看错误码信息,需要的错误码就是从errno来,程序出现结果不正确,错误码被设置,也就是errno被设置。
  3. 我们也可以自己设计一套对应的退出码

头文件:#include <string.h>
函数声明:char* strerror(int errnum)
参数:错误码(全局变量errno存放的就是错误码)
返回值:错误码对应的信息

代码:

#include <stdio.h>    
#include <stdlib.h>    
#include <errno.h>    
#include <string.h>    
    
int main()    
{    
 int *p = (int*)malloc(1000*1000*1000*4);    
    if(p == NULL)    
    {    
        printf("error msg:%s\n", strerror(errno));                                                                                                      
        exit(errno);    
    }    
    return 0;    
} 

运行结果:
运行结果

系统中设置的错误码信息

一共133个非常多

测试代码:

#include <stdio.h>    
#include <string.h>    
    
int main()    
{    
    for(int i = 0; i < 140; i++)    
    {    
        printf("strerror[%d] -> msg:%s\n", i, strerror(i));                                                                                                                                                                                 
    }    
    return 0;    
}  

运行结果: (注:没有全部截下来,太长了)
运行结果

perror

测试代码:

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

int main()                            
{                                           
	int *p = (int*)malloc(4*1000*1000*1000);
    if(p == NULL)                           
    {                                 
        perror("malloc error");       
        exit(errno);                                                                                                                                                                                                       
    }                                                                                                                                                                                                                      
    return 0;                                                                                                                                                                                                              
} 

测试结果:
测试结果
直接打印出错误信息。
可以这样理解:strerror+errno+printf == perror

异常信息

kill -l 查看信号
kill -[信号编号] [进程PID] 对指定进程发生信号

查看所有信号:
查看信号

SIGHUP:挂起信号,通常由终端关闭或网络连接中断引起。用于通知进程重新加载配置文件或进行清理操作。
SIGINT:中断信号,通常由终端用户通过键盘输入Ctrl+C发送。用于请求进程终止。
SIGQUIT:退出信号,通常由终端用户通过键盘输入Ctrl+\发送。与SIGINT类似,但会生成核心转储文件。
SIGILL:非法指令信号,表示进程执行了一个非法的机器指令。
SIGTRAP:跟踪陷阱信号,用于调试目的。
SIGABRT:异常终止信号,通常由调用abort函数引起。
SIGBUS:总线错误信号,表示进程访问了无效的内存地址。
SIGFPE:浮点异常信号,表示进程执行了一个非法的浮点运算。
SIGKILL:强制终止信号,用于立即终止进程。不能被忽略或捕获。
SIGUSR1:用户自定义信号。
SIGSEGV:段错误信号,表示进程访问了无效的内存段。
SIGUSR2:用户自定义信号。
SIGPIPE:管道破裂信号,表示进程向一个已关闭的管道写入数据。
SIGALRM:闹钟信号,用于定时器操作。
SIGTERM:终止信号,用于请求进程正常终止。
SIGSTKFLT:协处理器栈错误信号。
SIGCHLD:子进程状态改变信号,用于通知父进程子进程的状态发生了改变。
SIGCONT:继续信号,用于恢复被SIGSTOP或SIGTSTP暂停的进程的执行。
SIGSTOP:停止信号,用于暂停进程的执行。
SIGTSTP:终端停止信号,通常由终端用户通过键盘输入Ctrl+Z发送。用于请求进程暂停。
SIGTTIN:后台读取信号,表示后台进程试图从终端读取数据。
SIGTTOU:后台写入信号,表示后台进程试图向终端写入数据。
SIGURG:紧急条件信号,表示进程收到了一个紧急数据。
SIGXCPU:CPU时间限制信号,表示进程超过了CPU时间限制。
SIGXFSZ:文件大小限制信号,表示进程试图创建一个超过文件大小限制的文件。
SIGVTALRM:虚拟定时器信号。
SIGPROF:性能分析器定时器信号。
SIGWINCH:窗口大小改变信号,表示终端窗口大小发生了改变。
SIGIO:异步I/O信号。
SIGPWR:电源故障信号。
SIGSYS:无效系统调用信号,表示进程执行了一个无效的系统调用。

信号部分,再进行介绍

3. 退出方法

exit和_exit

  1. exit
#include <stdio.h>    
#include <stdlib.h>    
                                                                                                                                                                                                                                           
int main()    
{    
    printf("hello Linux");    
    exit(0);    
    return 0;    
} 

代码运行结果:
运行结果

  1. _exit
#include <stdio.h>    
#include <unistd.h>    
    
int main()    
{    
    printf("hello Linux");    
    _exit(0);                                                                                                                                                                                                                               
    return 0;    
}  

代码运行结果:
运行结果

结论:通过结果发现exit刷新了缓冲区,_exit没有刷新缓冲区

exit和_exit的底层关系:
exit和_exit的底层关系

总结:

  1. exit是库函数,_exit是系统调用接口
  2. exit会刷新缓冲区,_exit不会刷新缓冲区
  3. C语言的exit的实现底层封装系统调用接口_exit
  4. 系统调用接口不刷新缓冲区,所以得出缓冲区不在内核中,在用户空间

注: exit和return的区别

  1. exit是直接退出进程,而return是函数的返回值
  2. 在main函数中的return与exit的作用相同。在其它函数return和exit作用不同

三、进程等待

1. 解决等待的三个问题

  1. 是什么?
    通过系统调用wait/waitpid,对子进程进行状态检测与回收功能
  1. 为什么?
    • 僵尸进程无法被kill -9杀死(因为僵尸进程已经死了),需要通过进程等待来杀掉它,进而解决内存泄漏问题。
    • 可以通过进程等待,获取子进程的退出情况,可以了解子进程的任务完成情况。
    • 总而言之,父进程可以通过等待,回收子进程资源,获取子进程退出信息,无论父进程是否关心这个信息。
  1. 怎么办?
    父进程通过调用wait/waitpid进行子进程回收。

2. 系统调用

wait

头文件:
1. #include <sys/types.h>
2. #include <sys/wait.h>

函数声明:
pid_t wait(int *status);

参数:输出型参数,执行结束status会带出进程的退出状态(包括错误信息和异常信息)

返回值:成功返回所等待进程的pid,失败返回-1
参数为NULL

eg1:(简单使用)

#include <stdio.h>    
#include <unistd.h>    
#include <errno.h>    
#include <stdlib.h>    
#include <sys/wait.h>    
#include <sys/types.h>    
    
int main()    
{    
  pid_t id = fork();    
  if(id == 0)    
  {    
    //child    
    printf("I am child, pid: %d\n", getpid());    
  }    
  else if(id < 0)    
  {    
    perror("fork:");    
    exit(errno);    
  }    
  else    
  {    
    sleep(5);    
    pid_t ID = wait(NULL);    
    if(ID == id)                                                                                      
    {    
        printf("等待成功!\n");    
    }    
    else    
    {    
      printf("等待失败!\n");    
    }    
  }    
  sleep(3);    
  return 0;    
} 

预期结果:创建子进程,子进程执行完代码先等待三秒,然后父进程等待五秒,其中差额的两秒子进程进入僵尸状态,然后父进程休眠完之后等待成功,再等待三秒结束进程。

实验结果: (符合预期结果)
实验结果

eg2:(多个子进程进行等待)

#include <stdio.h>                                                                                    
#include <unistd.h>    
#include <sys/types.h>    
#include <sys/wait.h>    
#include <errno.h>    
#include <stdlib.h>    
    
#define N 3    
  
void RunChild()    
{    
  int cnt = 5;    
  while(cnt--)    
  {    
    printf("I am Child Process, pid: %d, ppid: %d\n", getpid(), getppid());    
    sleep(1);    
  }    
}      
    
int main()    
{    
  for(int i = 0; i < N; i++)    
  {    
    pid_t id = fork();    
    if(id == 0)    
    {    
      RunChild();    
      exit(i);    
    }    
    printf("create child process:%d success\n", id); //这句话只有父进程才会执行    
  }    
    
  sleep(10);    
  //开始等待    
  for(int i = 0; i < N; i++)    
  {    
    pid_t id = wait(NULL);
    if(id > 0)
    {
      printf("回收进程:%d->%d\n", i, id);
    }
  }

  sleep(5);

  return 0;
}

实验结果:在回收子进程的过程中,等到谁就释放谁
实验结果

也可以使用while :; do ps axj | head -1; ps axj | grep test | grep -v grep;sleep 1; done;查看进程状态

使用status参数

使用输出型参数status的原因:

  1. 因为进程间的独立性,所以父进程不能直接访问子进程
  2. 通过返回值返回的信息成功要返回子进程pid,无法再返回别的信息。

status:

  1. 该参数可以不设置,就如同上面直接用NULL,表示不关心子进程的退出状态信息。
  2. 正常设置该参数,OS会根据该参数,将子进程的退出信息反馈父进程。
  3. status也不能用简单的整型看待,要从位图看待(只研究低16位)

退出信息位分布:(低16位)
status低16位

来两个小demo测试一下:

正常终止:

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

int main()
{
  pid_t id = fork();
  if(id == 0)
  {
    //child
    printf("I am child, pid: %d\n", getpid());
    exit(11);
  }
  else if(id > 0)
  {
    //father
    int status = 0;
    pid_t ID = wait(&status);

    if(ID == id)
    {
      printf("子进程的退出信息:%d\n", status);
      printf("等待成功!\n");
    }
    else
    {
      printf("等待失败!\n");
    }
  }
  else
  {
    perror("fork");
    exit(11);
  }
                                                                                                                                                                                                                 
  return 0;
}

运行结果:
运行结果

异常终止:

#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>

int main()
{
  pid_t id = fork();
  if(id == 0)
  {
    //child
    printf("I am child, pid:%d\n", getpid());

    //出现异常
    int *p = NULL;
    *p = 0;
  }
  else if(id > 0)
  {
    int status = 0;
    pid_t ID = wait(&status);
    if(ID == id)
    {
      printf("子进程的退出信息:%d\n", status);                                                                                                                                                                  
      printf("等待成功!\n");
    }
    else
    {
      printf("等待失败!\n");
    }
  }
  else
  {
    perror("fork:");
    exit(errno);
  }
  return 0;
}

运行结果:
运行结果

小结

根据上文的测试:大致可以观察到测试结果和退出信息的位分布结果是一致的。

如何从位中获得具体的退出状态或者终止信号

  1. 退出状态:(status >> 8) & 0xFF
  2. 终止信号:(status & 0x7F)

系统也提供了一些宏,来帮助获取退出状态与终止信号

  1. 判断是否正常退出:WIFEXITED
    获取退出状态:WEXITSTATUS
  2. 判断是否被信号终止:WIFSIGNALED
    获取信号信息:WTERMSIG

根据上面的小结,再做一些小实验:

#include <stdio.h>    
#include <sys/types.h>    
#include <sys/wait.h>    
#include <unistd.h>    
#include <stdlib.h>    
#include <errno.h>    
    
int main()    
{    
  pid_t id = fork();    
  if(id == 0)    
  {    
    //child             
    printf("I am child, pid:%d\n", getpid());                                    
                                                                                 
    //出现异常                                                                   
    int *p = NULL;                                                               
    *p = 0;                                                                      
  }                                                                              
  else if(id > 0)                                                                
  {                                                                              
    int status = 0;                                                              
    pid_t ID = wait(&status);                                                    
    if(ID == id)                                                                 
    {                                                                            
      if(WIFEXITED(status))                                                      
      {                                                                          
        //printf("正常退出,退出状态:%d\n", WEXITSTATUS(status)); //和下面一条语句结果一样
        printf("正常退出,退出状态:%d\n", (status >> 8) & 0xFF);                
      }                                                                          
      if(WIFSIGNALED(status))                                                    
      {                                                                          
        //printf("异常退出,终止信号:%d\n", WTERMSIG(status));   //和下面一条语句结果一样                                                                                                                       
        printf("异常退出,终止信号:%d\n", status & 0x7F);  
      }                                                                                 
      printf("等待成功!\n");                                                            
    }                                                                                   
    else                                                                                
    {                                                                                   
      printf("等待失败!\n");                                                            
    }                                                                                   
  } 
  else 
  {
    perror("fork:");
    exit(errno);
  }
  return 0;
}
     

运行结果:
运行结果

注: 这个测试,只是测试了异常终止的代码,正常结束的程序就不再赘述了。

waitpid

头文件:
1. #include <sys/types.h>
2. #include <sys/wait.h>

函数声明:
pid_t waitpid(pid_t pid, int *status, int options);

参数:
	pid:
		1. pid = -1,等待任意一个子进程。与wait的作用一样
		2. pid > 0。等待与pid相等的子进程
	
	status:(该参数在wait部分已经详细介绍)
		WIFEXITED:正常终止的子进程,则为真。(查看进程是否正常退出)
		WEXITSTATUS:WEXITSTATUS不为0,提取子进程退出码。(查看进程的退出码)
	
	options:
		WNOHANG:若pid指定的子进程没有结束,则waitpid()函数返回0,不进行等待。若正常结束,则返回该子进程pid
		0:进行阻塞等待
		WUNTRACED:如果子进程进入暂停状态就立刻返回。

返回值:
	1. 当正常返回的时候waitpid返回的是收集到子进程的PID
	2. 如果设置了第三个参数WNOHANG,而调用waitpid发现没有已退出的子进程可收集,则返回0
	3. 调用出错,则返回-1,errno也会被设置

注: 在这里就不再对waitpid进行实验,在下一节内容顺带实验

3. 阻塞和非阻塞等待

阻塞等待:

#include <stdio.h>    
#include <stdlib.h>    
#include <unistd.h>    
#include <sys/types.h>    
#include <sys/wait.h>    
#include <errno.h>    
    
int main()    
{    
  pid_t pid = fork();    
  if(pid < 0)    
  {    
    perror("fork");    
    exit(errno);    
  }    
  else if (pid == 0)    
  {    
    //child    
    printf("child Running, pid:%d\n", getpid());    
    sleep(5);    
    exit(140);    
  }    
  else    
  {    
    int status = 0;    
    pid_t ret = waitpid(-1, &status, 0);   //第一个参数代表任意子进程都可以再次等待,第三个参数就是阻塞等待    
    printf("wait......5s..\n");                                                                                                                                                                                  
    if(WIFEXITED(status) && ret == pid)    
    {    
      printf("等待成功,ret_code:%d\n", WEXITSTATUS(status));    
    }    
    else    
    {    
      printf("等待失败!\n");    
      exit(errno);    
    }    
  }    
  return 0;    
} 

运行结果:
运行结果

非阻塞等待:

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <errno.h>
#include <unistd.h>

#define TASK_NUM 10

typedef void(*task_t)();  //定义函数指针
task_t tasks[TASK_NUM];   //函数指针数组

void task1()
{
  printf("执行打印日志的任务,pid:%d\n", getpid());
  //...
}

void task2()
{
  printf("检测网络健康状态的任务,pid:%d\n", getpid());
  //...
}

void task3()
{
  printf("绘制图形界面的任务,pid:%d\n", getpid());
}

int AddTask(task_t t);

//管理任务
void InitTask()
{
  for(int i = 0; i < TASK_NUM; i++)
  {
    tasks[i] = NULL;                                                                                                                                                                                             
  }
  AddTask(task1);
  AddTask(task2);
  AddTask(task3);
}
int AddTask(task_t t)                                                                                                                                                                                            
{
  int pos = 0;
  for(; pos < TASK_NUM; pos++) //寻找空位置
  {
    if(!tasks[pos])
      break;
  }

  //再判断边界
  if(pos == TASK_NUM)
    return -1;

  //插入任务
  tasks[pos] = t;
  return 0;
}

//执行任务
void ExecuteTask()
{
  for(int i = 0; i < TASK_NUM; i++)
  {
    if(!tasks[i]) //遍历指针数组
      continue;
    tasks[i]();
  }
}

int main()
{
  pid_t id = fork();
  if(id < 0)
  {
    perror("fork");
    exit(errno);
  }
 else if(id == 0)
  {
    //child
    int cnt = 5;
    while(cnt--)
    {
      printf("I am child, pid:%d, ppid:%d, cnt:%d\n", getpid(), getppid(), cnt);
      sleep(1);
    }
    exit(11);
  }
  else 
  {
    int status = 0;
    InitTask();
    while(1)  //非阻塞轮询
    {
      pid_t ret = waitpid(id, &status, WNOHANG);  //阻塞只需要该WNOHANG为0即可
      if(ret > 0)
      {
        if(WIFEXITED(status))
        {
          printf("代码正常跑完,退出码:%d\n", WEXITSTATUS(status));
        }
        else 
        {
          printf("代码异常!\n");
        }
        break;
      }
      else if(ret < 0)
      {
        printf("wait failed!\n");
        break;
      }
	  else  //轮询,处理别的任务 
      {
        ExecuteTask();
        sleep(1);
      }
    }
  }
  return 0;
}

代码运行结果:
运行结果

总结: 阻塞等待和非阻塞等待

  1. 阻塞等待:直到子进程被等待成功,才会继续执行,否则就一直处于阻塞状态
  2. 非阻塞等待:无论能不能等待到子进程都进行返回,等待成功返回子进程pid,失败返回0
  3. 非阻塞等待相比阻塞等待,可以在子进程没结束的时候,做一些自己的事情。所以也称作非阻塞轮询
  4. wait函数就是阻塞等待,子进程不结束,就一直等

4. 进程等待的原理

  1. 子进程运行结束,父进程没有等待之前,子进程是为僵尸状态,其代码和数据先被释放,PCB保留在OS。
  2. 称为僵尸进程的子进程的PCB中有这样的两个变量,int exit_code, exit_signal
  3. 父进程调用系统调用wait进行等待,因为进程间的独立性,实际由OS获取这个两个变量,并写入输出型参数当中。

结构图

四、进程替换

1. 概念

进程替换:进程替换需要通过调用exec系列的函数,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。注:不会创建新的进程,会更新PCB,但是如进程的PID值这样的不会改变。很像是被小说中的大能夺舍,变得是灵魂,不变的是肉体。

2. exec函数族

exec系列函数:
	头文件:#include <unistd.h>

返回值:
	1. 函数调用成功,则加载新的程序,开始执行代码,不返回
	2. 替换失败,返回-1.按照原先的代码继续运行。错误码被设置

①execl

函数声明:
	int execl(const char *path, const char *arg, ...);

参数:
	1. path:指的是所要打开文件具体的路径
	2. arg:所要打开的文件名
	3. ...:可变参数列表,传的是具体选项,且以NULL结尾

测试:

#include <stdio.h>    
#include <unistd.h>    
#include <sys/types.h>    
    
int main()    
{    
  pid_t id = fork();    
  if(id == 0)    
  {    
    //进行进程替换    
    int ret = execl("/usr/bin/ls", "ls", "-l", NULL);    
    //int ret = execl("ls", "ls", "-l", NULL);        //这个填的路径是相对路径,就是相对当前路径来说
    //执行ls -l命令    
    if(ret == -1)    
    {    
      printf("进程替换失败!\n");    
    }    
  }                                                                                                                
  return 0;    
} 

测试结果: (进程替换成功)
测试结果

②execlp


函数声明:
		int execlp(const char *file, const char *arg, ...);
参数:
	1. file:指的是所要打开文件的路径,若不加路径,可以在当前路径和PATH环境变量下的路径寻找
	2. arg:所要打开的文件名
	3. ...:可变参数列表,传的是具体选项,且以NULL结尾	

测试:

#include <stdio.h>    
#include <unistd.h>    
#include <sys/types.h>    
    
int main()    
{    
  pid_t id = fork();    
  if(id == 0)    
  {    
    //进行进程替换    
    int ret = execlp("ls", "ls", "-l", NULL);                                                                      
    //执行ls -l命令    
    if(ret == -1)    
    {    
      printf("进程替换失败!\n");    
    }    
  }    
  return 0;    
}

测试结果:
测试结果

③execle

函数声明:
		int execle(const char *path, const char *arg, ..., char *const envp[]);
参数:
	1. path:指的是所要打开文件具体的路径
	2. arg:所要打开的文件名
	3. ...:可变参数列表,传的是具体选项,且以NULL结尾
	4. envp:新的环境变量数组,即新执行程序的环境变量

测试:

#include <stdio.h>    
#include <unistd.h>    
#include <stdlib.h>    
    
int main()    
{    
  printf("before exec: USER=%s, HOME=%s\n", getenv("USER"), getenv("HOME"));    
    
  char *const env[] = {(char *const)"USER=KKK", (char *const)"HOME=hhhh", NULL};    
  int ret = execle("./process", "process", NULL, env);                                                                                                                                                                                    
  if(ret == -1)    
  {    
    perror("execle");    
    exit(11);    
  }    
    
  printf("After\n");    
  return 0;    
}   

替换的程序:

#include <stdio.h>                                                                                                                                                                                                                          
#include <cstdlib>    
#include <unistd.h>    
    
int main()    
{    
  printf("USER=%s\n", getenv("USER"));    
  printf("HOME=%s\n", getenv("HOME"));    
  return 0;    
}  

测试结果:
运行结果

注意:因为一次编译了两个程序,所以我们在编译makefile时,要借用为目标,让其推到自己编译好两个程序

makefile文件:

.PHONY:ALL                                                                                                                                                
ALL:test process    
process:process.cpp    
  g++ -o $@ $^    
    
test:test.c    
  gcc -o $@ $^ -std=c99    
.PHONY:clean    
clean:    
  rm -f test process 

④execv

函数声明:
		int execv(const char *path, char *const argv[]);
参数:
	1. path:替换程序的路径
	2. argv[]:保存的是参数列表,将可执行文件和参数保存到字符串数组中,最后以NULL结尾。

测试:

#include <stdio.h>    
#include <sys/types.h>    
#include <unistd.h>    
    
int main()    
{    
  char *const argv[] = { (char *const)"ls", (char *const)"-a", (char *const)"-l", NULL };                                                                 
  pid_t id = fork();    
  if(id == 0)    
  {    
    int ret = execv("/usr/bin/ls", argv);    
    if(ret == -1)    
    {    
      printf("进程替换失败!\n");    
    }    
  }    
  return 0;    
} 

测试结果:
测试结果

⑤execvp

函数声明:
		int execvp(const char *file, char *const argv[]);
参数:
	1. file:所要打开的文件路径(绝对和相对路径)。也可以在PATH环境变量下寻找
	2. argv:保存的是参数列表,将可执行文件和参数保存到字符串数组中,最后以NULL结尾。

测试:

#include <stdio.h>      
#include <unistd.h>      
#include <sys/types.h>      
      
#define ARR_NUM 4                                                                                                                                         
      
int main()      
{      
  char *const argv[ARR_NUM] = { (char *const)"ls", (char *const)"-a", (char *const)"-l", NULL};      
  pid_t id = fork();      
  if(id == 0)      
  {      
    int ret = execvp("ls", argv);      
    if(ret == -1)      
    {      
      printf("进程替换失败!\n");      
    }      
  }      
  return 0;      
}  

测试结果:
测试结果

⑥execvpe

该函数接口是GNU扩展,所以在使用的时候要加上#define _GNU_SOURCE

函数声明:
		int execvpe(const char *file, char *const argv[], char *const envp[]);
参数:
	1. file:指的是所要打开文件的路径,也可在环境变量PATH下寻找
	2. argv:指的是所要打开文件的名及选项
	3. envp:要传入的环境变量数组

测试代码:(test进程,其中子进程替换成process进程)

#include <stdio.h>    
#include <stdlib.h>    
#include <unistd.h>    
#include <sys/types.h>    
    
#define ARR_NUM 3    
    
extern char** environ;    
    
int main()    
{    
  char *const argv[ARR_NUM] = { (char *const)"process", NULL };    
  char *str =(char *)"MY_VAL=449220104";                                                                                                                  
  putenv(str);    
  pid_t id = fork();    
  if(id == 0)    
  {    
    int ret = execvpe("./process", argv, environ);    
    if(ret == -1)    
    {    
      printf("进程替换失败!\n");    
    }    
  }    
  return 0;    
} 

process:

#include <iostream>    
using namespace std;    
    
int main(int argc, char *argv[], char *env[])                                                                                                           
{    
  for(int i = 0; env[i]; i++)    
  {    
    cout << "env[" << i << "]: " << env[i] << endl;    
  }    
  return 0;    
}  

测试结果比较长,这里就不展示了。前面再介绍execle函数时,也说了如何编译多个程序

小结

上面我们介绍了exec系列的六种函数接口。这六个函数接口都包含在库文件中,而在这六个接口的底层,无疑调用了系统调用,这个系统调用接口就是execve。

六个函数的命名理解:
l (list):表示参数采用列表
v (vector):参数用数组
p (path):有p自动搜索环境变量PATH
e (env):表示自己维护环境变量

函数接口参数格式是否带路径是否使用当前环境变量
execl列表不是
execlp列表
execle列表不是需要自己组装环境变量
execv数组不是
execvp数组
execvpe数组需要自己组装环境变量
execve(系统调用)数组不是需要自己组装环境变量

代码:

  #include <unistd.h>    
      
  int main()    
  {    
    char *const argv[] = { "ps", "-axj", NULL };    
    char *const envp[] = { "PATH=/bin:/usr/bin", "TERM=console", NULL };    
      
    execl("/bin/ps", "ps", "-axj", NULL);    
      
    //带p的,可以使用环境变量PATH    
    execlp("ps", "ps", "-axj", NULL);    
      
    //带e的,需要自己组装环境变量    
    execle("ps", "ps", "-axj", NULL, envp);    
      
    execv("/bin/ps", argv);    
      
    //带p的,可以使用环境变量PATH    
    execvp("ps", argv);    
      
    //带e的,需要自己组装环境变量    
    execve("/bin/ps", argv, envp);    
      
    //带p和e的    
    execvpe("ps", argv, envp);                                                                                                                            
    return 0;    
  }    
    

六个函数的关系图:(去掉了GNU扩展的那个函数)
关系图

3. 系统调用——execve

函数声明:
		int execve(const char *path, char *const argv[], char *const envp[]);
参数:
	1. path:替换程序的路径
	2. argv:保存的是参数列表,可执行文件和参数保存到字符串数组中,以NULL结尾
	3. envp:新的环境变量数组

测试:

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

int main() {
    char *const argv[] = { "ls", "-l", NULL };
    char *const envp[] = { "PATH=/bin", NULL };

    int ret = execve("/bin/ls", argv, envp);
    if (ret == -1) {
        perror("execve");
        exit(EXIT_FAILURE);
    }

    return 0;
}

测试结果:
测试结果

4. 总结

  1. 子进程被进程替换之后,不会影响父进程。
    虽然说父子进程共享代码和数据,而进程替换也是替换代码和数据,但是本质是由OS再开一块物理空间,将页表的映射关系更改即可
  1. C代码可以替换C++程序是因为:操作系统对于正在运行的程序来说,无论是什么语言编写的,它们最终都会被操作系统视为进程来执行。最终都会被编译成机器码,以二进制形式存储在可执行文件中,当操作系统加载可执行文件并创建进程时,它会将这些机器码加载到内存中,并按照指令的顺序执行。
  1. 环境变量:子进程默认继承父进程的环境变量,但是进程的独立性,所以父子进程的环境变量也是相互独立的,子进程环境变量的修改不会影响父进程。eg:Shell的环境变量就是在用户登陆时从配置文件.bash_profile中加载
  1. 进程的入口:Linux形成的可执行程序是有格式的ELF格式,ELF格式定义了可执行文件的结构和布局。可执行程序被加载到内存时,OS会先读取可执行文件的表头。表头中包含程序的入口地址(程序开始执行的第一个指令的内存地址)
小知识:讲这个的原因,是因为说到了C代码可以替换C++程序,所以进行扩展
test.sh --> 全称.Shell --> Shell脚本
Shell脚本就是把Linux命令放到一个文件
开头-->#!(shebang-->用于指定脚本文件的解释器。作用就是告诉OS用那个解释器执行脚本文件) 所以要紧跟脚本语言对应的解释器。

-----------------------------------
test.py:
#!/usr/bin/python3
print("hello Python!")
-----------------------------------
test.sh:
#!/usr/bin/bash
function myfun()
{
	cnt=1
	while [ $cnt -le 10]
	do
		echo "hello $cnt"
		let cnt++
	done
}
echo "hello 1"
ls -a -l
myfun
-----------------------------------

替换上面的程序:
execl("/usr/bin/bash", "bash", "test.sh", NULL);
execl("/usr/bin/python3", "python3", "test.py", NULL);

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/374383.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

使用pandas将excel转成json格式

1.Excel数据 2.我们想要的JSON格式 {"0": {"raw_data1": "Sam","raw_data2": "Wong","raw_data3": "Good","layer": "12v1"},"1": {"raw_data1": "Lucy…

外汇天眼:大量的平庸操作无济于事,少数的杰出操作改变人生

回顾我们的人生&#xff0c;道路虽然漫长&#xff0c;经历虽然众多&#xff0c;但紧要处其实只有关键的几步。 在关键时刻处理得最好的人就会拥有最成功的事业; 回顾我们所交过的朋友&#xff0c;虽然数量像天上的繁星&#xff0c;但真正对自己的人生有重大影响的&#xff0c;不…

NUUO 网络摄像头命令执行漏洞

一、设备简介 NUUO NVR是中国台湾省NUUO公司旗下的一款网络视频记录器&#xff0c;该设备存在远程命令执行漏洞&#xff0c;攻击者可利用该漏洞执行任意命令&#xff0c;进而获取服务器的权限。 网络视频记录器的CPU为Marvell Kirkwood 88F6281&#xff0c;CPU架构为基于ARMv5…

LLM(大语言模型)——大模型简介

目录 概述 发展历程 大语言模型的概念 LLM的应用和影响 大模型的能力、特点 大模型的能力 涌现能力&#xff08;energent abilities&#xff09; 作为基座模型支持多元应用的能力 支持对话作为统一入口的能力 大模型的特点 常见大模型 闭源LLM&#xff08;未公开源…

GADM 4.1 全球国家行政区划下载

扫描文末二维码&#xff0c;关注微信公众号&#xff1a;ThsPool 后台回复g004&#xff0c;领取最新 GADM 4.1 全球国家行政区划 GADM概述 GADM&#xff0c;全称 Database of Global Administrative Areas&#xff0c;是一个开放获取的全球行政区划数据库&#xff0c;包含各国、…

智慧城市与数字孪生:技术驱动下的城市治理与生活变革

一、引言 随着科技的飞速发展&#xff0c;智慧城市和数字孪生已经成为现代城市发展的重要趋势。它们通过运用先进的信息通信技术&#xff0c;提升了城市的治理效率和居民的生活品质。本文将探讨智慧城市与数字孪生如何共同推动城市治理与生活的变革&#xff0c;以及面临的挑战…

Webpack源码浅析

webpack启动方式 webpack有两种启动方式&#xff1a; 通过webpack-cli脚手架来启动&#xff0c;即可以在Terminal终端直接运行&#xff1b; webpack ./debug/index.js --config ./debug/webpack.config.js通过require(webpack)引入包的方式执行&#xff1b;其实第一种方式最终…

踩坑了,MySQL数据库生成大量奇怪的大文件

作者&#xff1a;田逸&#xff08;formyz&#xff09; 一大早就收到某个数据库服务器磁盘满的报警信息&#xff0c;其中数据盘使用率超过90%&#xff0c;如下图所示。 这是一台刚上线不久的MySQL从库服务器&#xff0c;数据盘的总容量是300G。先登录系统&#xff0c;查看主从同…

一般系统的请求认证授权思路【gateway网关+jwt+redis+请求头httpheader】

gateway&#xff1a;网关&#xff0c;我们都知道网关的作用就是对系统的所有请求&#xff0c;网关都会进行拦截&#xff0c;然后做一些操作&#xff08;例如&#xff1a;设置每个请求的请求头httpHeader&#xff0c;身份认证等等&#xff09;此时一般会使用到网关过滤器&#x…

感悟笔记——2024年2月5日

今日阅读了一篇挺有深度的文章&#xff0c;主要阐述进入职场后的大部分人&#xff0c;是怎么逐渐沦为螺丝钉的?即使起点巨高的优等生&#xff0c;也不可避免。文章指路&#xff1a; 「优等生思维」正在将你变成「螺丝钉」和「老黄牛」从小到大&#xff0c;我一直都是那个「别…

随记-Java项目处理SQL注入问题

现象&#xff1a;http://10.xx.xx.xx:xx/services/xxService 存在SQL注入情况 加固意见&#xff1a; 需要对网站所有参数中提交的数据进行过滤&#xff0c;禁止输入“"、"xor"、"or"、”--“、”#“、”select“、”and“等特殊字符&#xff1b;所有…

专业排版设计软件:QuarkXPress 2024 for mac中文激活版

QuarkXPress 2024 for Mac是一款功能强大、易于使用、高质量输出的专业排版软件。无论您是出版业的专家还是初学者&#xff0c;都可以通过QuarkXPress 2024轻松创建出令人惊叹的出版物。 软件下载&#xff1a;QuarkXPress 2024 for mac中文激活版下载 QuarkXPress 2023 for Mac…

游戏后端如何实现服务器之间的负载均衡?

在当今的游戏行业中&#xff0c;随着游戏用户数量的不断增加&#xff0c;如何实现服务器之间的负载均衡成为了一个亟待解决的问题。游戏后端作为游戏的重要组成部分&#xff0c;承载着游戏逻辑处理和数据存储等功能&#xff0c;因此游戏后端的负载均衡问题尤为重要。本文将详细…

FINN: 使用神经网络对网络流进行指纹识别

文章信息 论文题目&#xff1a;FINN: Fingerprinting Network Flows using Neural Networks 期刊&#xff08;会议&#xff09;&#xff1a;Annual Computer Security Applications Conference 时间&#xff1a;2021 级别&#xff1a;CCF B 文章链接&#xff1a;https://dl.ac…

centos7的git使用方法

下载git yum install git git克隆 git clone https...(图片中复制的内容) git提交到远程仓库 git add filename git commit -m "提交日志" git push git首次使用要配置邮箱和用户名 查看提交日志 git log 查看当前提交状态 git status

JAVASE进阶:一文精通Stream流+函数式编程

&#x1f468;‍&#x1f393;作者简介&#xff1a;一位大四、研0学生&#xff0c;正在努力准备大四暑假的实习 &#x1f30c;上期文章&#xff1a;JAVASE进阶&#xff1a;源码精读——HashMap源码详细解析 &#x1f4da;订阅专栏&#xff1a;JAVASE进阶 希望文章对你们有所帮助…

【第三十五节】idea项目的创建以及setting和Project Structure的设置

项目创建 Project Structure的设置 点击file ~ Project Structure 进入

RBAC权限控制实现方案

上一文章讲述了利用RBAC实现访问控制的思路&#xff08;RBAC实现思路&#xff09;&#xff0c;本文主要详细讲解利用vuex实现RBAC权限控制。 一、准备工作 从后台获取到权限对照表&#xff0c;如下&#xff1a; 1、添加/编辑楼宇 park:building:add_edit 2、楼宇管理 pa…

3. ⼤语⾔模型深度学习背景知识

1. LLM⼤语⾔模型⼀般训练过程 #mermaid-svg-8kci1fjEPiVolPue {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-8kci1fjEPiVolPue .error-icon{fill:#552222;}#mermaid-svg-8kci1fjEPiVolPue .error-text{fill:#5522…

管理类联考-复试-全流程演练-导航页

文章目录 整体第一步&#xff1a;学校导师两手抓——知己知彼是关键学校校训历史 导师你对导师的研究方向有什么认知。 第二步&#xff1a;面试问题提前背——押题助沟通自我介绍——出现概率&#xff1a;100%为什么选择这个专业&#xff1f;今后如何打算&#xff1f;你认为自己…