Linux - 进程控制

我们用系统调用在代码内部对进程实现相关的操作,包括创建子进程/进程的退出/进程等待/进程替换程序内容执行其他程序。

进程创建

我们知道进程是可以被创建出来的,那么是谁创建了进程?大家可能想到的是被操作系统创建出来的,但只是部分进程由操作系统创建。

当用户登录的时候,操作系统会自动给用户分配一个shell。当用户要执行某个程序的时候,会通过某些操作来完成子进程的创建。同理我们自己写的程序也可以通过这个操作完成子进程的创建

fork创建子进程

通过fork函数来创建子进程

cpp 复制代码
#include <unistd.h>
pid_t fork(void);

fork会创建子进程,子进程会继承父进程的进程地址空间。相当于两个进程公用一套,具体细节我们下面再讲。

fork会返回pid_t,即进程的ID,当时父进程的时候,会返回子进程的pid,当是子进程的时候,返回0

cpp 复制代码
#include<iostream>
#include<unistd.h>
#include<sys/types.h>
using namespace std;
int main(){
	cout<<"start"<<endl;
	pid_t id = fork();
	if(id==0){
		cout<<"pid:"<<getpid()<<"ppid:"<<getppid()<<endl;
	}
	else{
		sleep(1);
	    cout<<"pid:"<<getpid()<<"ppid:"<<getppid()<<endl;
    }
    return 0;
}

这里也能推出来,后打印的是父进程,先打印的是子进程。而且我们发现start只打印的一次。原因是因为继承后的父子进程只会从fork处往下执行,并不会从头开始执行。

写时拷贝

大家是否有疑问,既然子进程继承了附近的程序地址空间,如果子进程改变某个地址的值,父进程的也会改变吗?

我们测试一下:

cpp 复制代码
#include<iostream>
#include<unistd.h>
#include<sys/types.h>
using namespace std;
int main(){
	cout<<"start"<<endl;
	pid_t id = fork();
    int x=0;
	if(id==0){
        x=2;
        printf("%p\n",&x);
		cout<<"pid:"<<getpid()<<"ppid:"<<getppid()<<endl;
	}
	else{
		sleep(1);
        printf("%p\n",&x);
        cout<<"x:"<<x<<endl;
	    cout<<"pid:"<<getpid()<<"ppid:"<<getppid()<<endl;
    }
    return 0;
}

我们发现并没有改变,而且这是必须的,否则就违背了进程的独立性。进程之间不能相互干扰。在接收pid的时候就已经有体现了,明明子进程是继承的父进程的进程地址空间,为什么会有不一样的值的变量。

但是还是有疑问,他们的地址时一样的啊?

原因是进程地址空间的地址是虚拟地址,子进程进程过去的时候,虚拟地址时直接拷贝过去的,哪个变量存储在同一个虚拟地址上面。但是物理地址不同。这里就设计到了页表,页表就设计到了虚拟地址到物理地址的转换。当创建子进程后,会将原本可写可读的地址权限变为只读的权限,如果有某一方要写当前的物理地址,就会触发中断,为当前进程创建新的物理地址。这样做的原因是为了节省空间。

用法

可以让子进程替父进程做一些事情,这样父进程就可以专注于做一件事。

进程退出

程序在满足某些情况后需要退出

退出场景

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

进程常见退出方式

_exit()

不会执行任何操作(例如缓冲区刷新)直接退出,并回收空间

exit()

这个是C++标准库对_exit的封装,它会执行用户注册的函数并刷新缓冲区再退出

注册函数atexit(void(*)())

通过传入一个函数指针来注册程序退出前完成的函数。例如保存相关内容,以免数据丢失等

cpp 复制代码
#include<iostream>
using namespace std;
void save(){
	cout<<"save all txt"<<endl;
}
int main(){
	atexit(save) ;
	exit(0);
}

最终就会执行save函数

如果使用_exit就不会执行

abort()

会触发对应信号,给进程自身发送退出信号并退出。若开启了ulimit -c unlimited,会生成core.xxx文件,用gdb a.out core.xxx可调试崩溃原因。同时它会跳过用户注册函数和缓冲区刷新。

退出码

退出码(退出状态)可以告诉我们最后⼀次执⾏的命令的状态。在命令结束以后,我们可以知道命令

是成功完成的还是以错误结束的。其基本思想是,程序返回退出代码 0 时表⽰执⾏成功,没有问题。

代码1 或 0 以外的任何代码都被视为不成功。

进程等待

进程等待的必要性

之前讲过,子进程退出,父进程如果不管不顾,就可能造成"僵尸进程"的问题,进而造成内存泄漏。

另外,进程一旦变成僵尸状态,那就刀枪不入,"杀人不眨眼"的kill-9也无能为力,因为谁也没有办法杀死一个已经死去的进程。

最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息

wait

cpp 复制代码
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);

会直接等待所有的子进程。需要传入一个返回形参数

status

我们知道退出码是8个字节,因此正常终止时,低8位就是0,高8位是退出码;被信号杀时,即异常退出,低七位为终止信号

status提取函数

我们可以用位运算自己提取,但是有现成的:

WIFEXITED(wait if exited)
cpp 复制代码
#include<sys/types.h>
#include<sys/wait.h>
int WIFEXITED(int status);

判断是否是正常退出的,非零为真

WIFSIGNALED(wait if signaled)
cpp 复制代码
#include<sys/types.h>
#include<sys/wait.h>
int WIFSIGNALED(int status);

判断是否是信号所杀的,非零为真

WIFSTOPPED(wait if stopped)
cpp 复制代码
#include<sys/types.h>
#include<sys/wait.h>
int WIFSTOPPED(int status);

判断是否是信号停止的,非零为真

WEXITSTATUS(wait exit status)

获取退出码,范围是0-255

WTERMSIG(wait terminal signal)

获取信号编号,来判断是哪个信号

WSTOPSIG(wit stop signal)

获取信号编号,来判断是哪个信号。因为暂停信号的status位不一样,因此要用这个函数来提取。

WCOREDUMP(wait coredump)

获取coredump的标志位,1表示生成了core dump危机文件,否则表示没有

waitpid

这个比wait用的更广泛,因为它的功能更加强大

cpp 复制代码
#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *status, int options);
pid

pid=-1,表示等待任意一个子进程,和wait等效

pid>0,表示等待指定的子进程

status

上面说过不说了,和wait一样

当我们不想接收状态的时候,可以传空指针

options

options传参是将多个状态位运算在一起

传 0 表示阻塞等待子进程

传 WNOHANG(wait no hang),表示非阻塞等待,不能和0或在一起,因为两个是独立的

传 WUNTRACED(wait un traced),表示接受等待继续信号,只有传了这个才能去接收等待继续的信号。用WIFSTOPPED/WSTOPSIG才有作用

返回值

正常接收返回子进程的pid。失败返回-1。如果设置了WNOHANG,如果没有等待到子进程则会返回0。

总结和举例

说实话这部分用的并不是很多,获取子进程退出状态并提取的情况可能比较少。下面是一个简单的代码

或者是用WNOHANG的,这样可以让父进程边运行边等待:

进程程序替换

既然有了子进程,那么子进程可以为我们干很多事情,特别是在CPU多核的现在,进程是可以并发运行的。因此有子进程可以大大提高运行的效率。因此我们可以让子进程安排任务:进程程序替换就是一个方式:

替换原理

当一个程序调用exec函数的时候,该进程的用户空间代码和数据就完全被新程序所替换,从新程序的启动历程开始执行。调用exec并不会创建新的进程,进程的pid也不会改变。

替换函数

cpp 复制代码
#include <unistd.h>

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

返回值

当调用成功就没有返回值,因为会被替换成新程序的代码,出错时会返回-1.

命名理解

l(list):表示参数采用列表

v(vector):表示参数采用数组argv

p(path):表示参数自动搜索环境变量下的程序

e(env):表示自己维护环境变量

cpp 复制代码
#include <unistd.h>
int main()
{
    char *const argv[] = {"ps", "-ef", NULL};
    char *const envp[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL};
    execl("/bin/ps", "ps", "-ef", NULL);
    // 带p的,可以使⽤环境变量PATH,⽆需写全路径
    execlp("ps", "ps", "-ef", NULL);
    // 带e的,需要⾃⼰组装环境变量
    execle("ps", "ps", "-ef", NULL, envp);
    execv("/bin/ps", argv);
    // 带p的,可以使⽤环境变量PATH,⽆需写全路径
    execvp("ps", argv);
    // 带e的,需要⾃⼰组装环境变量
    execve("/bin/ps", argv, envp);
    exit(0);
}

简单实现子进程运行命令

自主实现shell命令行解释器

大家看完上面的东西后,应该知道怎么实现了。

环境变量

首先shell创建会在系统文件里面获取环境变量,方便搜寻命令来执行,我们就不那么严格了,直接运行继承真正shell的环境变量就行了。下面是相关操作函数:

setenv

cpp 复制代码
#include <stdlib.h>

int setenv(const char *name, const char *value, int overwrite);

name代表环境变量名,value是值,overwrite为0表示如果存在不覆盖,1表示存在也覆盖

成功返回0,失败-1

getenv

cpp 复制代码
#include <stdlib.h>

char *getenv(const char *name);

获取环境变量名

做个测试

为什么要提到环境变量,原因就是要实现内置命令例如cd,export,echo,exit,pwd

工作路径

**这里我们使用setenv对PWD修改来模拟cd是正确的吗?并不正确,正真的工作路径存储在PCB里面,需要我们调用chdir系统调用改变。**因此正确做法是chdir修改工作路径,同时修改PWD的拷贝。

这里挑的几个常用的方便实现的

相关推荐
a程序小傲2 小时前
中国邮政Java面试被问:Netty的FastThreadLocal优化原理
java·服务器·开发语言·面试·职场和发展·github·哈希算法
重生之绝世牛码2 小时前
Linux软件安装 —— zookeeper集群安装
大数据·linux·运维·服务器·zookeeper·软件安装
额1292 小时前
磁盘物理卷、卷组、逻辑卷管理
linux·运维·服务器
Maggie_ssss_supp2 小时前
Linux-正则表达式
linux·运维·正则表达式
是娇娇公主~2 小时前
C++集群聊天服务器(3)—— 项目数据库以及表的设计
服务器·数据库·c++
重生之绝世牛码2 小时前
Linux软件安装 —— kafka集群安装(SASL密码验证)
大数据·linux·运维·服务器·分布式·kafka·软件安装
努力的小帅2 小时前
Linux_多线程(Linux入门到精通)
linux·多线程·多进程·线程同步·线程互斥·生产消费者模型
晴天¥2 小时前
操作系统由MBR->GPT,导致系统黑屏是怎么回事?
linux