当前位置: 首页 > article >正文

【Linux】进程控制和Shell的简易实现

1.进程创建

fork函数

pid_t fork()函数就从已存在进程中创建一个进程,新进程为子进程,而原进程就为父进程。
头文件:#include <sys/types.h> #include <unistd.h>
返回值:子进程就返回0,父进程返回当前子进程id,出错返回-1

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

  • 分配新的内存块和内核数据结构给子进程
  • 将父进程的部分数据结构内容拷贝给子进程
  • 添加子进程到进程的系统进程
  • fork返回,父子进程分别从 fork() 之后的代码开始执行,调度器开始调度进程
    在这里插入图片描述
    在这里插入图片描述

写时拷贝

通常,父子代码是共享的,父子不再写入时候,数据也是共享的,进程共享父进程的内存空间,但内核会标记为 只读 ,当任意一方试图写入,内核会为进程复制一份私有副本,防止进程之间相互影响
在这里插入图片描述
因为有了写时拷贝技术的存在,父子进程就可以数据就可以·分离开来,保证了进程之间的独立性
写时拷贝,是一种延时申请内存的技术,可以提高内存的利用率

fork常规用法

  • 父进程创建子进程,父子进程执行不同的代码段,例如:父进程等待客户端请求,子进程来处理请求或者用子进程来执行另一个不同的程序,子进程通过exec()进程替换来执行另一个任务

fork调用失败原因

  • 系统中有太多进程,而进程条目的数量是有限的,就会导致创建子进程失败
  • 实际用户的进程数受到了限制

fork() 行为总结

行为父进程子进程
fork() 返回值子进程 PID(>0)0
执行起点fork() 后的下一行代码fork() 后的下一行代码
内存修改触发写时拷贝(COW)触发写时拷贝(COW)
典型用途管理子进程执行新任务

(注意:父子进程两个执行流谁先执行完取决于调度器)

2.进程终止

进程终止的本质是释放系统资源,就是释放进程申请的相关内核结构和对应的代码和数据

进程退出场景

  1. 代码运行完毕,正确退出(从main返回,return 0)
  2. 代码运行完毕,异常退出 (exit(1))
  3. 代码异常终止·(ctrl + c)

退出码

我们通过 退出码 得到最近一次执行程序的退出状态,来了解程序是否正常退出
Linux 退出码:
在这里插入图片描述
我们可以通过strerror函数来查看对应退出码的描述

进程退出码查看

Linux 进程退出我们可以通过echo $?来查看进程退出码。
比如我正确执行一条指令,它的退出码就为0(Linux中执行指令都是通过bash创建子进程来执行的)
在这里插入图片描述

进程退出方法

  1. _exit函数
    _exit 函数是系统调用函数
#include <unistd.h>
void _exit(int status);

参数:status 定义了进程的终⽌状态,⽗进程通过wait来获取该值

  1. exit函数
    exit最后也会调⽤_exit, 但在调⽤_exit之前,还做了其他工作:
  • 执行用户通过atexiton_exit定义的清理函数。
#include <stdlib.h>
int on_exit(void (*function)(int , void *), void *arg);
#include <stdio.h>
#include <stdlib.h>

void cleanup(int status, void *msg) {
    printf("Exit status: %d, Message: %s\n", status, (char *)msg);
}

int main() {
    on_exit(cleanup, (void *)"Bye!");  // 注册带参数的清理函数
    printf("Main function\n");
    exit(42);  // 退出状态 42
}

输出:

Main function
Exit status: 42, Message: Bye!
  • 关闭所有打开的流,所有缓存数据均被写入
  • 调用_exit
    在这里插入图片描述
    exit和_exit函数的区别:exit是对_exit系统调用封装的库函数,它就可以刷新用户缓冲区数据,也就是fflush函数刷新用户缓冲区数据到内核缓冲区中,并且还会调用atexit和on_exit清理函数
int main()
{
	printf("hello");
	exit(0);
}
运⾏结果:
[root@localhost linux]# ./a.out
hello[root@localhost linux]#
int main()
{
	printf("hello");
	_exit(0);
}
运⾏结果:
[root@localhost linux]# ./a.out
[root@localhost linux]#
  1. return返回
    return 是一种常见的退出进程方法,return n 等同于执行 exit(n),因为调用 main 的运行函数会将 main 的返回值当 exit 的参数。

3.进程等待

僵尸进程

僵尸进程就是已经终止但未被父进程回收的进程。

进程等待

如果想要对僵尸进程进行处理,就需要父进程父进程回收子进程的方式来解决子进程的僵尸状态。

进程等待方法

wait
  • 函数方法:
    #include<sys/types.h>
    #include<sys/wait.h>
    pid_t wait(int* status);
    参数: status:保存子进程退出状态(需用宏解析,如 WEXITSTATUS)。
    返回值: 成功:返回终止的子进程 PID。
    失败:返回 -1(如无子进程)。
#include <sys/wait.h>
#include <unistd.h>
#include <stdio.h>

int main() {
    pid_t pid = fork();

    if (pid == 0) 
    {
        // 子进程
        printf("Child PID: %d\n", getpid());
        _exit(42);  // 子进程退出码 42
    } 
    else 
    {
        // 父进程
        int status;
        pid_t child_pid = wait(&status);  // 阻塞等待
        printf("Child %d exited with status: %d\n", 
               child_pid, WEXITSTATUS(status));
    }
    return 0;
}

输出:

Child PID: 1234
Child 1234 exited with status: 42
waitpid
  1. 函数方法:
    pid_ t waitpid(pid_t pid, int *status, int options);
    返回值:
    当正常返回的时候waitpid返回收集到的⼦进程的进程ID;
    如果设置了选项WNOHANG,⽽调⽤中waitpid发现没有已退出的⼦进程可收集,则返回0;
    如果调⽤中出错,则返回-1,这时errno会被设置成相应的值以指⽰错误所在;
    参数:
    pid:
    Pid=-1: 等待任⼀个⼦进程。与wait等效。
    Pid>0: 等待其进程ID与pid相等的⼦进程。
    status: 输出型参数
    WIFEXITED(status): 若为正常终⽌⼦进程返回的状态,则为真。(查看进程是否是正常退出)
    WEXITSTATUS(status): 若WIFEXITED⾮零,提取⼦进程退出码。(查看进程的退出码)
    options: 默认为0,表⽰阻塞等待
    WNOHANG: 若pid指定的⼦进程没有结束,则waitpid()函数返回0,不予以等
    待。若正常结束,则返回该⼦进程的ID。

等待子进程:

pid_t child_pid = fork();

if (child_pid == 0) 
{
    // 子进程
    _exit(10);
} 
else 
{
    int status;
    // 只等待 child_pid 的子进程
    waitpid(child_pid, &status, 0);
    printf("Child %d exited: %d\n", child_pid, WEXITSTATUS(status));
}

非阻塞轮询:

int status;
pid_t pid = waitpid(-1, &status, WNOHANG);

if (pid > 0)
{
    printf("Child %d exited.\n", pid);
}
else if (pid == 0)
{
    printf("No child exited yet.\n");
} 
else
{
    perror("waitpid failed");
}
获取子进程status
  1. wait和waitpid,都有⼀个status参数,该参数是⼀个输出型参数,由操作系统填充。

  2. 如果传递NULL,表⽰不关心⼦进程的退出状态信息。

  3. 否则,操作系统会根据该参数,将子进程的退出信息反馈给传递父进程

status 可以当做一个位图来看,其具体实现细节如下图:
在这里插入图片描述

退出状态和终止信号以及core dump表示方式

1.正常终止
退出状态:(status >> 8)&0xFF
2. 被信号所杀
退出信号:status&0x7F
core dump(核心转储):(status>>7)&1

进程替换

替换原理

fork 创建的子进程和父进程执行的是同一个程序,如果子进程想要执行另一个程序,往往需要调用 exec 函数来替换当前程序。当进程调用 exec 程序后,进程代码和数据就完全被新进程替换,从新程序的启动例程开始执行,并且执行完新进程后并不会返回之前子进程执行的代码继续执行,而是直接退出,并且调用exec并不会创建新进程,所以调用exec前后该进程的id并没有改变
​返回值处理: 所有函数调用成功时不返回,失败时返回-1并设置errno。

替换函数

其中有六个exec开头的函数,统称exec函数:

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 execve(const char *path, char *const argv[], char *const envp[]);
函数名参数传递方式路径处理环境变量处理是否系统调用其他特点
execl可变参数列表需完整路径(如/bin/ls继承当前环境变量参数以 NULL 结尾(如 "ls", "-l", NULL
execlp可变参数列表PATH 搜索文件名继承当前环境变量自动搜索可执行文件路径(如 execlp("ls", ...)
execle可变参数列表需完整路径自定义 envp 数组参数列表后需显式传递 envp(如 execle(..., envp)))
execv参数数组 argv[]需完整路径继承当前环境变量参数数组需以 NULL 结尾(如 char *argv[] = {"ls", "-l", NULL}))
execvp参数数组 argv[]PATH 搜索文件名继承当前环境变量结合路径搜索与数组传参(如 execvp("ls", argv))
execve参数数组 argv[]需完整路径自定义 envp 数组唯一直接调用内核的系统调用(其他函数均封装它)

使用方法:

  1. execl
    特点:参数列表形式传参、需完整路径、继承环境变量
#include <unistd.h>
#include <stdio.h>

int main() {
    printf("execl调用示例\n");
    // 执行/bin/ls,参数列表需以NULL结尾
    if (execl("/bin/ls", "ls", "-l", NULL) == -1) {
        perror("execl失败");
    }
    return 0;
}
  1. execlp
    特点:参数列表传参、自动搜索PATH环境变量
#include <unistd.h>
#include <stdio.h>

int main() {
    printf("execlp调用示例\n");
    // 自动搜索PATH中的"ls"可执行文件
    if (execlp("ls", "ls", "-l", NULL) == -1) {
        perror("execlp失败");
    }
    return 0;
}
  1. execle
    特点:参数列表传参、需完整路径、自定义环境变量
#include <unistd.h>
#include <stdio.h>

int main() {
    char *envp[] = {"CUSTOM_ENV=test", "PATH=/bin", NULL};
    printf("execle调用示例\n");
    // 传递自定义环境变量envp
    if (execle("/bin/ls", "ls", "-l", NULL, envp) == -1) {
        perror("execle失败");
    }
    return 0;
}
  1. execv
    特点:参数数组传参、需完整路径、继承环境变量
#include <unistd.h>
#include <stdio.h>

int main() {
    char *argv[] = {"ls", "-l", NULL};
    printf("execv调用示例\n");
    if (execv("/bin/ls", argv) == -1) {
        perror("execv失败");
    }
    return 0;
}
  1. execvp
    特点:参数数组传参、自动搜索PATH环境变量
#include <unistd.h>
#include <stdio.h>

int main() {
    char *argv[] = {"ls", "-l", NULL};
    printf("execvp调用示例\n");
    if (execvp("ls", argv) == -1) {
        perror("execvp失败");
    }
    return 0;
}
  1. execve
    特点:参数数组传参、需完整路径、自定义环境变量、唯一系统调用
#include <unistd.h>
#include <stdio.h>

int main() {
    char *argv[] = {"ls", "-l", NULL};
    char *envp[] = {"CUSTOM_ENV=test", "PATH=/bin", NULL};
    printf("execve调用示例\n");
    if (execve("/bin/ls", argv, envp) == -1) {
        perror("execve失败");
    }
    return 0;
}
总结

总结下来就是 是否需要完整路径、自定义环境变量,以及参数是列表还是数组

这些函数原型看起来很容易混,但只要掌握了规律就很好记。

  1. l(list) : 表示参数采用列表
  2. v(vector) : 参数用数组
  3. p(path) : 有p⾃动搜索环境变量PATH
  4. e(env) : 表示自己维护环境变量
    在这里插入图片描述

4,简单shell的实现

**前言:**我们在命令行执行命令时都是由 bash 创建子进程,然后由子进程 exec进程替换 执行对应命令。
在这里插入图片描述
shell脚本的流程:

  1. 获取命令行
  2. 解析命令行
  3. fork()创建子进程
  4. 替换子进程
  5. 父进程等待子进程退出

源码实现:

#include <iostream>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <errno.h>
using namespace std;

const int basesize = 1024;
const int gnum = 64;//环境变量表和命令行参数表的大小
char* genv[gnum];
char* gargv[gnum];
char buff[basesize];//辅助数组,存储输入的命令
char pwd[basesize];
char pwdenv[basesize];
int gargc = 0;
int lastcode = 0;

string getUsername()
{
    string username = getenv("USER");
    return username == "" ? "None" : username;
}
string getHostname()
{
    char hostname[20];
    int n = gethostname(hostname,sizeof(hostname));
    if(n < 0)
    {
        perror("gethostname");
        exit(1);
    }
    return hostname;
}
string GetPwd()
{
    if(getcwd(pwd,sizeof(pwd)) == nullptr) 
    {
        return "Node";
    }
    snprintf(pwdenv,sizeof(pwdenv),"PWD=%s",pwd);
    for(int i = 0;genv[i];i++)
    {
        string str = genv[i];
        char* before = (char*)str.substr(0,3).c_str();
        if(strcmp(before,"PWD") == 0)
        {
            strncpy(genv[i],pwdenv,strlen(pwdenv));
            genv[i][strlen(pwdenv)] = 0;
            return pwd;
        }
    }

}

string Lastdir()
{
    string cur = GetPwd();
    if(cur == "/" || cur == "None")
        return cur;
    size_t pos = cur.rfind('/');
    if(pos == std::string::npos) return cur;
    return cur.substr(pos + 1);
}

void PrintCommand()
{
    string username = getUsername();
    string hostname = getHostname();
    string pwd  = Lastdir();
    if(username != "None" && hostname != "None" && pwd != "None")
    {
        cout << username << "@" << hostname << ":" << pwd << "$";
        fflush(stdout);
    }
    else 
    exit(1);
}
bool GetCommand()
{
    memset(buff,0,sizeof buff);
    char* result = fgets(buff,basesize,stdin);
    if(result == nullptr)
    {
        return false;
    }
    //cout << result << endl;
    buff[strlen(buff) - 1] = 0;
    if(strlen(buff) == 0) return false;
    return true;
}
void ParseCommand()
{
    const char* sep = " ";
    gargc = 0;
    gargv[gargc++] = strtok(buff,sep);
    while((bool)(gargv[gargc++] = strtok(nullptr,sep)));
    //cout << gargc << endl;
    gargv[gargc] = nullptr;
    gargc--;
}
bool ExecuteCommand()
{
    pid_t id = fork();
    if(id < 0) return false;
    if(id == 0)
    {
        //子进程
        // 1. 执行命令
        cout << "gragvp[0] = " << gargv[0] << endl;
        int ret = execvpe(gargv[0], gargv, genv);
        //char* argv[] = {
        //    "ls",
        //    "-l",
        //    "-a",
        //    nullptr
        // };
        //int ret = execvpe("ls",gargv,genv);
        cout << errno << endl;
        lastcode  = 1;
        // 2. 退出
        exit(1);
    }
    int status = 0;
    pid_t rid = waitpid(id, &status, 0);
    if(rid > 0)
    {
        lastcode = WEXITSTATUS(status);
        return true;
    }
    lastcode = 100;
    return false;
    //pid_t id = fork();
    //if(id == 0)
    //{
    //    cout << gargv[0] << endl;
    //    int ret = execvpe(gargv[0],gargv,genv);
    //    if(ret == -1)
    //    {
    //        cout << errno << endl;
    //        return false;
    //    }
    //    return true;
    //}
    //int status;
    //pid_t wid = waitpid(id,&status,0);
    //if(wid > 0)
    //{
    //    cout << "等待子进程成功" << endl;
    //    return true;
    //}
    //return false;

}
void AddEnv()
{
    int index = 0;
    while(genv[index])
    {
        index++;
    }
    genv[index] = (char*)malloc(strlen(gargv[1] + 1));
    strncpy(genv[index],gargv[1],strlen(gargv[1]) + 1);
    genv[++index] = nullptr;

}
bool CheckCommand()
{
    if(strcmp(gargv[0],"cd") == 0)
    {
        if(gargc == 2)
        {
            chdir(gargv[1]);
            GetPwd();
            lastcode = 0;
        }
        else
        {
            lastcode = 2;
        }
        return true;
    }
    else if(strcmp(gargv[0],"echo") == 0)
    {
        if(gargc == 2)
        {
            if(gargv[1][0] == '$')
            {
                if(gargv[1][1] == '?')
                { 
                    printf("%d\n",lastcode);
                    lastcode = 0;
                }
            }
            else 
            {
                printf("%s\n",gargv[1]);
                lastcode = 0;
            }
        }
        else 
        lastcode = 3;
        return true;
    }
    else if(strcmp(gargv[0],"env") == 0)
    {
        for(int i = 0;genv[i];i++)
        cout << genv[i] << endl;
        lastcode = 0;
        return true;
    }
    else if(strcmp(gargv[0],"export") == 0)
    {
        if(gargc == 2)
        {
            AddEnv();
            lastcode = 0;
            return true;
        }
        else 
        lastcode = 4;
        return true;
    }

    return false;

}

void debug()
{
    for(int i = 0;genv[i];i++)
    {
        cout << genv[i] << endl;
    }
    cout << "//" << endl;
    for(int i = 0;gargv[i];i++)
    {
        cout << gargv[i] << endl;
    }
}
void Initenv()
{
    extern char** environ;
    int index = 0;
    while(environ[index])
    {
        genv[index] = (char*)malloc(strlen(environ[index] + 1));
        strncpy(genv[index],environ[index],strlen(environ[index]));
        index++;
    }
    genv[index] = nullptr;
}
int main()
{
    Initenv();
    while(1)
    {
        PrintCommand();//打印命令提示符
        //sleep(10);
        bool ret = GetCommand();//从标准输入获取命令
        if(ret = true)
            ParseCommand();//分析命令
        else 
            continue;
       // cout << endl;
      // debug();
       if(CheckCommand())
       {
           continue;
       }

       ExecuteCommand();//处理命令
    }


    return 0;
}


http://www.kler.cn/a/613377.html

相关文章:

  • 深入剖析Redis分布式锁:Redlock算法源码解读与实战
  • 【学Rust写CAD】15 定点数实现(fixed.rs)
  • Linux文件目录管理指令详解(下篇)
  • C语言之链表增删查改
  • CSS3学习教程,从入门到精通,CSS3 弹性盒子(Flexbox)布局全面指南(20)
  • 一款超级好用且开源免费的数据可视化工具——Superset
  • 前端技术有哪些
  • 微信小程序:数据拼接方法
  • 运维面试题(十一)
  • 明达网关云平台——开启透明化制造新时代
  • VMware Windows Tools 存在认证绕过漏洞(CVE-2025-22230)
  • Yolo_v8的安装测试
  • AI Agent浪潮下,昇腾与科大讯飞携手开辟AI落地“新航路”
  • Pycharm(七):几个简单案例
  • VectorBT:使用PyTorch+LSTM训练和回测股票模型 进阶二
  • GenBI 中如何引入 LLM 做意图路由,区分查数据还是闲聊
  • Android生态大变革,谷歌调整开源政策,核心开发不再公开
  • MAC环境给docker换源
  • 【力扣hot100题】(010)滑动窗口最大值
  • 项目接入通义千问 api 接口实现步骤详解