Linux下进程控制

创建进程

fork创建进程

fork函数

返回值:子进程中返回0,父进程返回子进程id,出错返回-1。

进程调用fork,当控制转移到内核中,内核做:

  • 分配新的内存块和内核数据结构给子进程
  • 将父进程的部分数据结构内容拷贝给子进程
  • 将子进程添加到系统进程列表当中
  • fork返回,调度器开始调度
c 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
    printf("before fork pid is :%d\n",getpid());
    pid_t id = fork();
    if(id < 0){
        perror("fork");
    }

    printf("after fork pid id :%d\n",getpid());
    
    printf("fork return value is :%d\n",id);
    return 0;
}

上面程序运行结果

可以看出,12行和14行中的打印信息被执行了两次,而且两次的值也不一样。 因为fork之前父进程单独执行,fork之后,父子两个执行流分别执行。注意,fork之后,谁先执行完全由调度器 决定。

fork返回值

  • 子进程返回0
  • 父进程返回的是子进程的id

写诗拷贝

fork之后,父子进程共享代码,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本。

fork常规用法

  • 一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子 进程来处理请求
  • 一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数。

fork失败的原因

  • 系统中有太多的进程
  • 实际用户的进程数超过了限制

进程终止

进程退出的场景

  • 代码跑完,结果正确。
  • 代码跑完,结果不正确
  • 代码没跑完,进程出异常

进程正常终止

  • main返回
  • 调用_exit
  • 使用exit

再平时写C/C++程序时,一般都i会这样写

c 复制代码
#include <stdio.h>
int main()
{
    //...
    return 0;
}

都会在main函数最后一行写一句return 0;

return 0;就是进程的退出码,一般0表示成功,非0 表示失败。 一但程序执行失败,需要知道原因,用不同的数字来表示不同的失败原因。

查看进程退出码

使用echo $?可以查看最近一次进程的退出码

比如系统中没有lll命令,执行,然后查看进程退出码,可以看到退出码为127

在C标准库函数中,有一个strerror函数,用于将错误码(通常是由系统调用或库函数返回的错误码)转换为对应的错误消息字符串。

c 复制代码
#include <stdio.h>
#include <string.h>
int main()
{
    for (size_t i = 0; i < 255; i++)
    {
        printf("error code %d:%s\n",i,strerror(i));
    }
    return 0;
}

Linux下错误码一共有133个。

_exit函数

参数:status 定义了进程的终止状态,父进程通过wait来获取该值 _exit在程序的任意位置都可以终止进程

c 复制代码
#include <stdio.h>
#include <unistd.h>
void test()
{
    printf("Hello World\n");
    _exit(1);
}
int main()
{
    test();
    return 0;
}

运行结果:

main函数中的return 0;还没有执行到进程就终止了。

exit函数

exit和_exit 的区别,exit是c语言的进程终止的函数,而_exit是Linux系统调用接口的函数,c语言在实现exit函数时会封装_exit。

c 复制代码
#include <stdio.h>
#include <stdlib.h>
void test()
{
    printf("Hello World\n");
    exit(1);
}
int main()
{
    test();
    return 0;
}

进程退出码和上面一样,都是1,main函数中的return 0;不会执行,进程就终止了。

exit 和 _exit 的区别

  • eixt会刷新缓冲区 更推荐
  • _eixt 不会刷新缓冲区。
  • exit封装了_exit;

同样的代码,分别使用eixt 和 _exit终止进程,exit会刷新缓冲区。(printf语句没有加\n),而_exit则不会刷新缓冲区。

return退出

  • return是一种更常见的退出进程方法。执行return n等同于执行exit(n),因为调用main的运行时函数会将main的返 回值当做 exit的参数。
  • 只有在main函数中return才能起到进程终止的作用,在其他函数中return不会终止进程,仅仅只是函数调用结束。

进程等待

进程等待的必要性

  • 子进程退出,父进程不等待,子进程可能变僵尸,从而造成内存泄漏
  • 一个进程一旦变为僵尸,谁都无能为力,kill也杀不掉,因为,无法杀掉一个已经死掉的进程。
  • 父进程创建子进程,子进程的任务完成的情况,我们需要知道。子进程的运行结果是否正确,是否正常退出(比如ls命令 是bash的子进程,ls的执行情况是需要我们知道的)
  • 父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息。(父进程必须要做的)

进程等待的方式

wait方法(系统调用)

man 2 wait 认识wait

返回值:

等待成功,返回子进程的pid

等待失败,返回-1。

函数参数: 输出型参数,获取子进程退出状态,不关心则可以设置成为NULL

wait:它等待任意子进程退出,而不需要指定特定的子进程ID。如果没有子进程退出,wait 会阻塞当前进程直到有子进程退出。(阻塞等待简单理解就是父进程啥也不干,就只等子进程退出进行回收)

c 复制代码
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
int main()
{
    pid_t id = fork();
   if(id < 0 )
   {
    perror("fork\n");
   }
   else if(id == 0){
    //子进程
    printf("i am child process.pid = %d\n",getpid());
    //子进程退出 退出码为1 
    exit(1);
   }
   else{
     //父进程
    printf("i am Parent process waiting...\n");
    sleep(3);//父进程等待三秒后回收子进程
    int status = 0;
    pid_t waitid = wait(&status);//不想获取子进程退出码可以设置为NULL
    //等待失败处理
    if(waitid == -1)
    {
        perror("wait fail\n");
        exit(-1);
    }
    printf("wait sucess, child excit code = %d,waitid = %d\n",status,waitid);
   }
    return 0;
}

运行结果:

这里父进程获取到的子进程退出码并不是1,而是256 这是因为status占4个字节,32个比特位。前十六位不用关心,后是6位前8位表示进程正常的退出码,最后七位表示进程异常收到的信号,退出码和信号中间的一位是core dump标志位,和信号有关,这里不用深究。

exit(1) 则是进程正常退出,也没有收到异常信号,status则是

这就是256的原因。 如果想直接拿到进程的退出码,而不是status,(status>>8)& 0xff即可 上面代码31行修改为

c 复制代码
 printf("wait sucess, child excit code = %d,waitid = %d\n",(status>>8)&0xFF,waitid);

运行结果:

waitpid方法(系统调用)

等待指定进程或者任意子进程,相比wait更灵活 man 2 waitpid 认识waitpid

参数

  • pdi:等待子进程的id,若设置为-1,则和wait等效
  • status:输出型参数,获取子进程的退出状态,不关心子进程退出状态设置为NULL
  • options:设置等待方式,阻塞等待或者非阻塞等待

返回值

  • 等待成功返回被等待进程的pid。
  • 等待方式设置为非阻塞等待(WNOHANG),而调用中waitpid发现没有已退出的子进程可收集,则返回0;

阻塞等待示例:

c 复制代码
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
int main()
{
    //waitpid阻塞等待
    pid_t id = fork();
    if(id < 0 ){
        perror("fork\n");
        exit(-1);
    }
    else if(id == 0){
        //子进程
        printf("i am child process. pid = %d\n",getpid());
        sleep(3);
        exit(1);
    }
    else {
        //父进程
       printf("parent process waiting ...\n");
       int status = 0 ;
       pid_t ret = waitpid(-1,&status,0);//阻塞等待任意子进程
      
       if(WIFEXITED(status) && ret == id){//等待成功
        printf("wait sucess,child return code = %d,ret = %d\n",WIFEXITED(status),ret);
       }
       else{//等待失败
        printf("wait fail\n");
       }
    }
    return 0;
}

运行结果:

等待成功,子进程退出码为1,waitpid返回值为子进程的pid

WEXITSTATUS 和 WIFEXITED

WIFEXITEDWEXITSTATUS 是在 <sys/wait.h> 头文件中定义的两个宏,用于处理子进程的退出状态信息。

  • WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
  • WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)

非阻塞等待示例:

cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
int main()
{
    //waitpid非阻塞等待
    pid_t id = fork();
    if(id < 0 ){//失败处理
        perror("fork\n");
        exit(-1);
    }
    else if(id == 0){//子进程
        printf("i am child process. pid = %d\n",getpid());
        sleep(3);
        exit(1);
    }
    else {//父进程
       printf("parent process waiting ...\n");
       int status = 0;
       pid_t ret  = 0;
       do
       {
            ret = waitpid(-1,&status,WNOHANG);//非阻塞等待任意子进程
            if(0 == ret){//子进程还没退出,父进程非阻塞等待可以做其他事情
                printf("child process is running,parent do other things\n");
                sleep(1);
            }
       }while(0 == ret);
       //等待成功
       if(WIFEXITED(status) && ret == id){
       //打印子进程退出码和waitpid返回值
        printf("wait sucess,child return code = %d,ret = %d\n",WEXITSTATUS(status),ret);
       }
       //等待失败
       else{
        printf("wait fail\n");
       }
    }
    return 0;
}

运行结果:

父进程非阻塞等待子进程时还可以做其他的事情, wiatpid返回值时子进程的id

阻塞等待和非阻塞等待

阻塞等待:

阻塞等待会导致进程无法执行其他任务,直到等待的事件发生。 waitpid函数设置为阻塞等待,只需将options参数设置为0

  • 阻塞等待的优点:

    • 实现简单,易于理解。
    • 不需要额外的代码来检查事件状态。
  • 阻塞等待的缺点:

    • 整个进程或线程被挂起,无法执行其他任务。
    • 可能导致资源浪费,因为进程被阻塞时,它可能无法充分利用系统资源

非阻塞等待

waitpid函数设置为阻塞等待,需要将options参数设置为WNOHANG。(一个宏)

  • 非阻塞等待的优点:

    • 可以充分利用系统资源,因为在等待事件的同时可以执行其他任务。
    • 更灵活,适用于需要同时处理多个任务的情况。
  • 非阻塞等待的缺点:

    • 实现较为复杂,需要额外的代码来轮询或处理事件通知。
    • 可能会增加系统负载,因为需要周期性地检查事件状态

进程程序替换

进程程序替换是指一个正在运行的进程将自己的地址空间、代码、数据和堆栈等信息替换为另一个程序的内容。

  • 用fork创建的子进程和父进程执行的是相同的程序,但有可能执行不同的代码分支。一般fork创建的子进程需要调用exec函数来执行另一个程序。
  • 当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。
  • 调用exec并不会创建新的子进程,所以调用exec前后该进程的id并未改变。
  • 程序替换是通过特定的接口,加载到磁盘上的一个程序,加载到调用进程的地址空间中

替换函数

有六种以exec开头的函数,统称exec函数:

c 复制代码
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[]);

execl函数

cpp 复制代码
int execl(const char *path, const char *arg, ...);
  • 参数1:path 是要执行的程序的路径(需要指定路径)。
  • 参数2:是可变参数列表,表示你要如何执行这个程序,并以NULL结尾。 示例:
cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
int main()
{
    //进程程序替换
    pid_t id = fork();
    if(id < 0){
        perror("fork fail\n");
    }
    else if(id == 0){
        //子进程
        printf("i am child process, pid =  %d",getpid());
        //子进程执行ls -a 命令
        execl("/usr/bin/ls","ls","-a",NULL);
        printf("exec end\n");
        exit(1);
    }
    else{
        //父进程
        int status = 0;
        pid_t  ret = waitpid(id,&status,0);//阻塞等待指定子进程
        if(WIFEXITED(status) && ret == id)//等待成功
        {
            printf("wait sucess,child return code = %d,ret = %d, id = %d\n",WEXITSTATUS(status),ret,id);
        }
    }
    return 0;
}

运行结果:

可以发现 当进程程序替换完成后,exec后面的代码将不再执行。 一旦exec替换失败,才会只执行后面的代码 比如将上面17行要替换的进程改为一个不存在的,

cpp 复制代码
execl("/usr/bin/lsl","ls","-l",NULL);

执行结果:子进程后面的代码被执行了。

execlp函数

cpp 复制代码
int execlp(const char *file, const char *arg, ...);
  • 参数1:需要执行的程序名称。只需要指定程序名称,不需要指定路径
  • 参数2:是可变参数列表,表示你要如何执行这个程序,并以NULL结尾。 示例:子进程执行ls命令
c 复制代码
execlp("ls","ls","-l",NULL);

execle函数

c 复制代码
int execle(const char *path, const char *arg, ..., char * const envp[]);

参数1:需要执行程序的路径,

参数2:是可变参数列表,表示你要如何执行这个程序,并以NULL结尾。

参数3:envp:是一个以 NULL 结尾的字符串数组,用于设置新程序的环境变量。

c 复制代码
// 设置新程序的环境变量
char *envp[] = {"MY_VARIABLE=value", NULL};
// 使用 execle 函数替换当前进程的程序
execle("/bin/echo", "echo", "Hello, execle!", (char *)NULL, envp);

运行结果:

execv函数

c 复制代码
int execv(const char *path, char *const argv[]);
  • 参数1:path 是要执行的程序的路径。
  • 参数2:argv 是一个以 NULL 结尾的指针数组,其中包含新程序的名称和参数。
c 复制代码
char *const argv[] = {"ls","-l",NULL};
execv("/usr/bin/ls",argv);

运行结果:

execvp函数

c 复制代码
int execvp(const char *file, char *const argv[]);

不用指定路径即可。

execvpe函数

c 复制代码
int execvpe(const char *file, char *const argv[], char *const envp[]);

第一个参数是要执行程序的路径,第二个参数是一个指针数组,数组当中的内容表示你要如何执行这个程序,数组以NULL结尾,第三个参数是你自己设置的环境变量。

exec系列函数

  • 这些函数如果调用成功,则加载指定的程序并从启动代码开始执行,不再返回。
  • 如果调用出错,则返回-1。
  • exec函数只有成功的返回值,没有失败的返回值。

exec家族关系

相关推荐
极客先躯21 分钟前
如何自动提取Git指定时间段的修改文件?Win/Linux双平台解决方案
linux·git·elasticsearch
suijishengchengde1 小时前
****LINUX时间同步配置*****
linux·运维
qiuqyue1 小时前
基于虹软Linux Pro SDK的多路RTSP流并发接入、解码与帧级处理实践
linux·运维·网络
切糕师学AI2 小时前
Linux 操作系统简介
linux
南烟斋..2 小时前
GDB调试核心指南
linux·服务器
爱跑马的程序员2 小时前
Linux 如何查看文件夹的大小(du、df、ls、find)
linux·运维·ubuntu
oMcLin5 小时前
如何在 Ubuntu 22.04 LTS 上部署并优化 Magento 电商平台,提升高并发请求的响应速度与稳定性?
linux·运维·ubuntu
Qinti_mm5 小时前
Linux io_uring:高性能异步I/O革命
linux·i/o·io_uring
优雅的38度5 小时前
linux环境下,使用docker安装apache kafka (docker-compose)
linux·架构
想唱rap5 小时前
表的约束条件
linux·数据库·mysql·ubuntu·bash