linux进程控制

本文详细解释了C语言中的fork函数工作原理,进程的独立性,以及如何通过write时拷贝实现内存管理。讨论了进程终止时的操作,如内存释放和退出码含义,以及各种进程管理函数wait、waitpid和execl的使用。最后介绍了如何实现简易shell,展示了进程管理和命令执行的基本概念。
摘要由CSDN通过智能技术生成

fork函数

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

  1. 分配新的内存块和内核数据结构给子进程
  2. 将父进程部分数据结构内容拷贝至子进程
  3. 添加子进程到系统进程列表当中
  4. fork返回(执行完),开始调度器调度

子进程的代码和数据

创建子进程,给子进程分配对应的内核结构(必须是子进程独有的,因为进程具有独立性),所以理论上,子进程要有自己的代码数据
但子进程并没有加载的过程,所以子进程只能使用"父进程的代码和数据".其中:

代码:不可被写,只能被读,所以父子共享,没有问题
数据:可能被修改,所以必须分离

写时拷贝

从上面可知,数据必须分离,通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本。
image-20240215204357437

os为何要选择写时拷贝:

  1. 用的时候再给你分配内存,是高效使用内存的一种表现
  2. os无法在代码执行前提前预知哪些空间会被访问

写时拷贝的好处:

  1. 因为有写时拷贝的出现,父子进程得以彻底分离,完成了进程独立性的技术保证
  2. 写时拷贝,是一种延时拷贝的策略,可以提高整机内存使用效率

fork之后,父子进程代码是after之后共享的,还是所有代码共享的?

image-20240215205503974

进程终止

进程终止时,操作系统做了什么?

释放进程申请的相关内核数据结构和对应的代码和数据 (本质就是释放系统资源(cpu和内存,主要是内存))

进程常见的终止方式

  • 代码运行完毕,结果正确代码
  • 运行完毕,结果不正确
  • 代码异常终止

对于前两条,思考,main函数的返回值的意义是什么?return 0的含义是什么?为什么总是0?

  1. 返回给上一级进程,是用来评判该进程执行结果用的,当我们的程序结果不正确时,方便定位错误原因和细节
  2. 进程的退出码
  3. 并不总是0,0代表success,非0表示结果不正确
echo $? 返回上一个命令的状态,0表示没有错误,其它任何值表明有错误
char *strerror(int errnum) 该函数返回一个指向错误字符串的指针,该错误字符串描述了错误 errnum。

对于第三条,当一个进程异常终止时,它的退出码无意义,一般而言退出码对应的return语句,没有被执行

用代码如何终止一个进程?

进程常见的退出方法

  • main函数内return返回
  • 库函数exit()
  • 系统调用_exit()

exit和_exit的区别?

image-20240302164026086

这其中的缓冲区是语言级别的缓冲区,是由c标准库帮助我们进行维护的

验证一下:

printf("you can see me");		// 不加\n,不会刷新缓冲区
sleep(3);
exit(123);                  
//_exit(123); 

image-20240302165016460

进程等待

作用

父进程通过进程等待的方式,回收子进程资源(防止子进程变为僵尸进程浪费空间),获取子进程退出信息

方法

#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int*status);
返回值:
成功返回被等待进程pid,失败返回-1。
参数:
输出型参数,获取子进程退出状态,不关心则可以设置成为NULL
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

实验

先生成僵尸进程

int pid = fork();
if(pid < 0)
{
  perror("fork:");
  exit(-1);
}
else if(pid == 0)
{
  //子进程
  int cnt=5;
  while(cnt--)
  {
    printf("child cnt:%d, pid:%d, ppid:%d\n", cnt, getpid(), getppid());
    sleep(1);
  }
  exit(0);
}
else 
{
  while(1)
  {
    printf("father, pid:%d, ppid:%d\n", getpid(), getppid());
    sleep(1);
  }
}

父进程可以使用wait或者waitpid来等待子进程状态变化

// 使用wait
printf("father, pid:%d, ppid:%d\n", getpid(), getppid());  
int ret = wait(NULL);  
if(ret)  
{  
  printf("等待子进程状态改变成功\n");   // 阻塞式的等待
} 
// 基本没有意义,只是为了对比演示                            
while(1)
{                                                            
  printf("father, pid:%d, ppid:%d\n", getpid(), getppid());
  sleep(1);
}
int main() {
	pid_t id = fork();
	if(id == 0) {
		int cnt=5;
		while(cnt--) {
			printf("子进程运行中, cnt:%d, id:%d\n", cnt, getpid());
			sleep(1);
		}
		exit(66);
	} else {
		int status = 0;
		// 第一个参数,pid
		// pid>0,等待指定进程
		// pid=-1,等待任意进程,与wait等效
		pid_t res = waitpid(id, &status, 0);
		if(res != 0) {
			// 可以不这么写
			// printf("等待子进程成功,exit_code:%d, sig_code:%d\n", (status>>8)&0xff, status&0x7f);
			if(WIFEXITED(status) == true) {
				// 子进程正常退出
				printf("子进程正常退出,退出码是:%d\n", WEXITSTATUS(status));
			} else {
				// 子进程异常退出
				printf("子进程异常退出,退出码是:%d\n", WIFEXITED(status));
			}
		}
	}
	return 0;
}

status 状态

  • wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充
  • 如果传递NULL,表示不关心子进程的退出状态信息
  • 否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程 ,status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特位)

image-20240303203223745

所以,父进程可以这样写

int status = 0;
int ret = wait(&status);
if(ret > 0 && (status & (0x7f))==0)  // 正常退出
{
  printf("正常退出,ret:%d, child exit code:%d\n",ret, (status>>8)&0xff);   // 阻塞式的等待
}
else if(ret>0)                                                                                     
{
  printf("异常退出,sig code:%d\n",status&0x7f);
}

此时,使用kill -9命令退出(此时退出码无意义,看子进程收到的信号编号)和正常退出(此时子进程收到的信号编号为0,看退出码)结果如下

image-20240303210138062

image-20240303210200052

非阻塞等待

typedef void (*handler_t)();  // 函数指针类型 
vector<handler_t> handlers;   // 函数指针数组
void func_one()
{
  cout<<"这是任务1"<<endl;
}
void func_two()
{
  cout<<"这是任务2"<<endl;
}
// 设置方法回调
void Load()
{
  handlers.push_back(func_one);
  handlers.push_back(func_two);
}


int main() {
    pid_t id = fork();
    if (id == 0) {
        int cnt = 5;
        while (cnt--) {
            printf("子进程运行中, cnt:%d, id:%d\n", cnt, getpid());
            sleep(1);
        }
        exit(66);
    }
    else {
        int quit = 0;
        while (!quit) {
            int status = 0;
            pid_t res = waitpid(-1, &status, WNOHANG);
            // 非阻塞式的等待
            if (res > 0) {
                // 等待成功&&子进程退出                                                        
                printf("等待子进程退出成功,退出码是:%d\n", WEXITSTATUS(status));
                quit = 1;
            }
            else if (res == 0) {
                // 等待成功&&子进程退出
                printf("子进程还没有退出,这时候父进程可以做一些别的事情\n");
                if(handlers.empty())  Load();
                for(auto&e : handlers){
                  // 执行其他任务
                  e();
                }
                sleep(1);
            }
            else {
                printf("wait等待失败!\n");
                quit = 1;
            }
        }
    }
    return 0;
}

这里的父进程可以做一些别的事情是什么呢?可以看下面的例子

问题

父进程通过wait/waitpid来拿到子进程的退出结果,用全局变量不可以吗?

当然不可以,进程具有独立性,那么就要发生写时拷贝,父进程无法拿到

既然进程是具有独立性的,进程退出码不也是子进程的数据吗?父进程又凭什么能拿到呢?wait/waitpid究竟做了什么?

通过看linux task_struct的源码可以知道,有一个exit_code,这个就是进程的退出码,所以wait/waitpid实际上就是读取子进程的task_struct结构中的exit_code
虽然进程具有独立性,但是父进程可以通过系统调用来获取子进程的退出码

进程程序替换

概念

程序替换,是通过特定的接口(exec()),将磁盘上的一个权限的程序(代码和数据),加载到调用进程的地址空间当中
比如当我们使用fork时,若子进程想要执行一个全新的程序

原理

image-20240417180446843

问题:进程替换,有没有创建新的进程?
没有!因为仅仅将一个程序加载到了内存,仅改变了页表,磁盘和物理内存的映射关系,进程pcb的优先级,pcb的内容并没有发生变化

#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
using namespace std;
int main()
{
    pid_t id = fork();
    if (id == 0)
    {
        // 子进程
        cout << "子进程开始执行,pid:" << getpid() << endl;
        sleep(3);
        execl("/usr/bin/ls", "ls", "-a", "-l", NULL);
        exit(66);
    }
    else
    {
        // 父进程
        cout << "父进程开始执行,pid:" << getpid() << endl;
        int status = 0;
        pid_t res = waitpid(-1, &status, 0); // 阻塞等待,一定是子进程先运行完毕,然后父进程获取之后,才退出
        if (res > 0)
        {
            cout << "等待子进程成功!退出码是:" << WEXITSTATUS(status) << endl;
        }
    }
    return 0;
}
  1. 创建一个子进程的意义:如果不创建,那么我们替换的进程就只能是父进程,如果创建了,替换的就是子进程的代码和数据,从而不影响父进程,因为进程具有独立性

  2. 加载新程序之前,父子进程的代码共享,子进程如果修改数据,则发生写时拷贝

当子进程加载新程序时(execl),就相当于发生了"写入",此时,代码也要发生写时拷贝,将父子进程代码发生分离,以防污染父进程代码

函数介绍

c语言提供了一共有6个execl函数

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[]);

int execv(const char *path, char *const argv[]);

前面是路径,后面是一个指针数组

char *_argvs[N] =
    {
        (char *)"ls",
        (char *)"-a",
        (char *)"-l",
        NULL
};
execv("/usr/bin/ls", _argvs);

int execlp(const char *file, const char *arg, …);

有p自动搜索环境变量PATH ,由于ls在环境变量PATH中,所以可以这样写

execlp("ls", "ls", "-a", "-l", NULL);   

如何让执行其他自己写的的程序(c,c++,python,java…)

假设现在在你的目录下有如下的文件

image-20240421180907149

其中mycmd.cc会形成mycmd程序,myproc.cc会形成myproc程序,现在我们想让myproc执行mycmd程序

// mycmd.cc
#include <iostream>
#include <stdlib.h>
#include <string.h>
using namespace std;
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        cout << "can not execut" << endl;
    }
    else if (strcmp(argv[1], "-a") == 0)
    {
        cout << "function a" << endl;
    }
    else if (strcmp(argv[1], "-b") == 0)
    {
        cout << "function b" << endl;
    }
    else
    {
        cout << "default!" << endl;
    }
    return 0;
}
// myproc.cc
execl("./mycmd", "myproc", "-a", NULL);  		// 也可以用绝对路径

int execle(const char *path, const char *arg,…, char * const envp[]);

可以给指定的进程设置环境变量,继续上面的例子,现在对mycmd.cc和myproc.cc修改一下

// mycmd.cc
cout<<"获取的环境变量是:"<<getenv("MY_VAL") << endl;   
char* _env[N] =
{
    (char*)"MY_VAL=123456789",
    NULL
};
execle("./mycmd", "myproc", "-b", NULL, _env);          

image-20240422131512525

总结

实际上,上面的函数都是c语言为了满足我们不同的需求提供的函数,这些函数在底层均调用了系统调用接口:

#include <unistd.h>

int execve(const char *filename, char *const argv[],
           char *const envp[]);

关于函数命名规则的理解

  • l(list) : 表示参数采用列表
  • v(vector) : 参数用数组
  • p(path) : 有p自动搜索环境变量PATH
  • e(env) : 表示自己维护环境变量

实现一个简易版的shell

#include <iostream>
#include <cstring>
#include <stdio.h>
#include <sys/wait.h>
#include <unistd.h>
#include <string>
#include <algorithm>
using namespace std;
const int N = 20;
string cmd_line;
char *g_argv[N]; // 用于储存分割后的命令
// shell运行原理: 通过让子进程执行命令,父进程等待&&解析命令
int main()
{
    // 命令行解释器一定是一个常住内存的解释器,不退出
    while (true)
    {
        // 1. 打印出提示信息
        cout << "[root@localhost myshell]# ";
        fflush(stdout); // 需要手动刷新缓冲区
        // 2. 获取用户的键盘输入
        getline(cin, cmd_line);
        // cout<<cmd_line<<endl;
        // 3. 命令字符串解析
        int i = 0; // g_argv[N]的下标
        while (cmd_line.find(" ") != string::npos)
        {
            int found = cmd_line.find(" ");
            string subStr = cmd_line.substr(0, found);
            g_argv[i] = new char[subStr.length() + 1];
            strcpy(g_argv[i++], subStr.c_str());
            cmd_line = cmd_line.substr(found + 1);
        }
        g_argv[i] = new char[cmd_line.length() + 1];
        strcpy(g_argv[i], cmd_line.c_str());
        g_argv[i++] = (char *)cmd_line.c_str();
        g_argv[i++] = NULL;
        //  4.内置命令,让父进程(shell)自己执行的命令,叫做内置命令或者内建命令
        if (strcmp(g_argv[0], "cd") == 0)
        {
            if (g_argv[1] != NULL)
                chdir(g_argv[1]);
        }

        // 5.fork()
        pid_t id = fork();
        cout << g_argv[0] << endl;
        if (id == 0)
        {
            // child
            cout << "下面的功能是子进程执行的" << endl;
            // 当执行cd命令(cd ..)时,只会影响子进程的当前目录,不会影响父进程的路径,所以有第四步
            execvp(g_argv[0], g_argv);
            exit(66);
        }
        // father
        int status = 0;
        pid_t ret = waitpid(id, &status, 0);
        if (ret > 0)
            cout << "退出码:" << WEXITSTATUS(status) << endl;
    }
    return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值