linux-进程控制

进程创建

进程创建我们使用的函数是fork函数,这是一个系统调用接口。那么fork究竟干了什么呢?子进程的创建是以父进程为模板的。当父进程去fork子进程的时候,它会

  • 分配新的内存块和内核的数据结构给子进程,也就是task_struct等等。
  • 将父进程的部分数据结构拷贝到子进程
  • 将子进程添加到系统的进程列表中
  • fork返回,开始调度。

fork
我们知道进程实际上就是代码+数据。在默认情况下,父子进程是代码共享,数据独有。这里的共享不仅仅是after的代码共享,是所有的代码共享(即使用if分流后,所有代码也是共享,不使用的代码不代表不属于进程。),before的代码也是共享。只不过在fork之后,由于pcb中有程序计数器,判断子进程从after开始执行。
那么为什么要代码共享呢? 首先代码是只可读的,在代码段。这保证了共享的可能性。其次,父子进程的执行逻辑完全相同,不共享简直浪费空间。
那么为什么数据要独有一份呢? 因为进程具有独立性!如果不共享,那么子进程的修改就能影响父进程。这样进程之间肯定会相互破坏!但是不是所有的数据都是立刻要被使用的,也就是说不需要所有的数据被拷贝一份。如果立刻全部拷贝到子进程,那么就可能造成不需要的数据被多拷贝了,浪费时间空间,操作系统采用的方法是什么时候使用,使用哪块数据,就只拷贝那一块,这叫做写时拷贝。

写时拷贝

所谓写时拷贝,顾名思义,写的时候再进行拷贝。一开始fork的时候,实际上父子进程的数据也是共享,此时操作系统会将数据也变成只读的,一旦一方进行写入操作,那么操作系统就会发现这个错误,然后进行写时拷贝,只将需要拷贝的一小块数据分配到新的内存中。
写时拷贝
可以看到,被修改的数据,父子进程的页表会改变只读权限,然后操作系统为子进程新选择一块内存,将它的数据映射过去,但是其他的数据保持不变!!

fork如何实现两个返回值?

fork函数在函数返回之前就已经创建出了子进程,所以return这个数据具有两份,父进程需要return,子进程同样需要return。

为什么fork给父进程返回子进程的pid,给子进程返回0?

因为父进程可以创建多个子进程,如果需要不同的子进程完成不同的工作,就需要父进程知道子进程的信息,这就是子进程的PID。而子进程只有一个父进程,不需要标记,所以返回0.

fork的常规用法

  • 父进程希望进程分流,创建子进程执行不同的分支,执行不同的代码段。
  • 利用程序替换,子进程执行不同的程序。

fork失败的原因

  • 系统进程过多,资源不够。
  • 一个用户创建的进程是有限的,用户创建的进程超过上限。
  • 进程终止

进程退出一般有三种场景:

  • 代码跑完退出,结果正确。

  • 代码跑完退出,结果不正确。

  • 代码没跑完,异常退出。

  • 情况2,这就好像小明想来一包老坛酸菜牛肉面,但是只吃到了香辣牛肉面。情况3就是小明没吃到方便面。
    那么我们执行一个代码就一定得知道它的结果对不对,否则就没有意义。前两种情况可以通过return的返回值来判断。return的返回值交给操作系统,0代表没问题。其他的数字分别代表出现不同的问题(可以通过 $?查看上一个进程的退出码)。而第三种情况下的退出,退出码就没有作用。我们需要看的是退出的异常提示。我们有三种常见的正常退出的方法:

  • 通过main函数的return。

  • C语言的库函数exit

  • 系统调用函数_exit

这三个有什么区别呢?

  • return代表当前函数退出,如果在main函数中才与exit相同。
  • exit 直接终止进程,但是终止之前会做一些清理工作,比如刷新缓冲区。
  • _exit 直接干掉进程,别的啥也不做。
    eixt && _exit
    如果使用exit,那么hehe会被打印,因为exit会刷新缓冲区。但是_exit的使用不会打印hehe。
    exit的常用法:我们知道父子进程共享代码,那么在fork的代码会被执行两次,如果有的代码不想子进程来执行,那么就可以使用exit来提前结束子进程。
// eixt的经典用法
#include <iostream>
#include <unistd.h>

using namespace std;

int main(){
	pid_t id = fork();
	if(id > 0){
		// 父进程
		cout << "I am father process." << endl;
	}
	else if(id == 0){
		// 子进程
		cout << "I am child process." << endl;
		exit(0); //子进程直接退出,不会执行下面的语句
	}
	else{ //出错
		cerr << "fork error." << endl;
	}
	//下面这句话我只想父进程执行,不想要字进程也执行。就可以使用exit。
	cout << "这句话只能被父进程打印。" << endl;
	return 0;
}

exit的参数

exit的参数是一个int型的参数,实际上它只有8个字节被使用。

//头文件之类的东西不再写

int main(){
	exit(11111);
	return 0;
}

上述进程,11111明显在整形范围内。按照我们的理解,返回值应该是11111.但是,我发现返回值是103。那么103和11111是什么关系呢?11111转换成二进制的后8位就是103!

11111 -> 0000 0000 0000 0000 0010 1011 0110 0111 (11111的二进制序列)
103 -> 0000 0000 0000 0000 0000 0000 0110 0111 (103的二进制序列)

没错,一个进程的返回值最多就只占8位,就是255个返回值。

进程等待

进程等待的意义

  • 进程等待要从僵尸进程说起。我们知道如果子进程的数据不被父进程接收,那么子进程就会变成僵尸进程。僵尸进程是很麻烦的,它刀枪不入,我们无法使用kill -9发送信号去干掉一个已经”死亡“的进程。

  • 但是我们必须知道子进程把我们交给它的任务完成的怎么样,也就是说我们需要接收子进程的返回码。我们只能够等待父进程挂掉,将子进程转换成孤儿进程,然后由systemd来接收子进程的返回码。

  • 所以我们需要父进程进行进程等待,接收子进程的退出码。

  • 你也可能会说,这不是必须的。因为你完全可以不要进程等待,就等父进程挂掉,让1号进程接管子进程就ok。但是这样做的缺点就是,如果父进程运行时间很长,甚至一直运行,子进程就一直占用系统资源,造成内存泄漏。

我们常使用的进程等待函数有wait和waitpid。
wait
其中wait的参数和waitpid的第二个参数一样。当我们在父进程里面使用wait/waitpid函数,父进程会堵塞(waitpid可以不阻塞),然后一直等到子进程运行完毕,返回退出码给父进程,然后继续工作。这里要介绍两个概念,阻塞和非阻塞:

  • 阻塞:为了完成某个功能发起的一个调用,如果条件不满足,则一直等待,知道条件满足。
  • 非阻塞:如果不满足实现这个功能的条件,直接报错返回。

wait :

  • 父进程等待子进程退出,将退出码放到stat_loc中,然后返回子进程的PID。
#include <iostream>
#include <unistd.h> 
#include <stdlib.h>  
#include <sys/wait.h>
using namespace std;

int main(){
	pid_t id = fork();
	if(id < 0){
		cerr << "fork error ." ; 
	}
	else if(id == 0){
		//child
		int count = 10;
		while(count){
			cout << "I am child." << endl;
			sleep(1);
			--count;
		}
		exit(0);
	}
	else{
		// father
		wait(NULL); //不关心子进程运行结果 
	}
	return 0;
}

上述代码父进程阻塞等待子进程,结果就是子进程先打印10次,然后父进程等待成功,打印“wait after”。
wait1

waitpid :

  • waitpid的第一个参数pid是用来表示等待的目标。如果为-1,表示等待任意子进程退出。如果pid > 0,那么等待进程号为pid的子进程退出。
  • 第二个参数是一个输出型参数,stat_loc指针指向的int变量中存储着程序的返回码或者报错信息。如果为NULL,表示不关系子进程的运行结果。
  • 第三个参数是参数选项。options为0,表示阻塞等待。options为WNOHANG,表示非阻塞等待。
  • RETURN VALUE:如果等待成功,则返回子进程的PID。如果有子进程,但是没有退出,则返回0。等待出错则返回-1。显然返回0是配合非阻塞等待使用。
  • waitpid、wait会去处理退出的子进程,而不管这个子进程退出多久。
  • 可以看出 wait(&status) == waitpid(-1, &status, 0);
#include <iostream>
#include <unistd.h> 
#include <stdlib.h>  
#include <sys/wait.h>
using namespace std;

int main(){
	pid_t id = fork();
	if(id < 0){
		cerr << "fork error ." ; 
	}
	else if(id == 0){
		//child
		int count = 10;
		while(count){
			cout << "I am child." << endl;
			sleep(1);
			--count;
		}
		exit(0);
	}
	else{
		// father
		pid_t ret = waitpid(id, NULL, WNOHANG); //非阻塞等待子进程PID为id的子进程
		if(ret == 0){ //子进程未退出
			cout << "child process is still running". << endl;
		}
	}
	return 0;
}

上面这个父进程采用非阻塞的方式等待子进程,但是子进程没有退出的时候父进程已经等待完毕。由于是非阻塞等待,所以还是没有达到进程等待的目的。还是会产生僵尸进程。所以,非阻塞等待一般配合循环使用。

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

int main(){
	pid_t id = fork();
	if(id < 0){
		cerr << "fork error ." ; 
	}
	else if(id == 0){
		//child
		int count = 10;
		while(count){
			cout << "I am child." << endl;
			sleep(1);
			--count;
		}
		exit(0);
	}
	else{
		// father
		int ret;
		while((ret = waitpid(id, NULL, WNOHANG)) == 0){ //循环等待子进程结束
			cout << "child process is still running" << endl;
			sleep(1);
		}
		if(ret > 0){
			cout << "wait success ." << endl;
		}
	}
	return 0;
}

非阻塞比起阻塞,它的好处是显然的:充分利用cpu资源。增加进程的执行效率。就好比你在河边钓鱼,你可以拿出一本C语言的书看一会书,看一眼鱼浮,而不是一直看着鱼浮。

关于waitpid的第二个参数

第二个参数是一个输出型参数status,它的内容由操作系统填充。它里面的内容是子进程的退出码和退出状态。但是1个int型的变量如果装载这么多信息呢?答案是,int型由32个比特位组成。我们将每一种错误信息映射成不同的数字即可。实际上,我们只研究status的低16位。
status

  • 如果一个进程是正常退出(即运行到了代码结束的地方),那么在status的低16位中的低8位全是0,高8位则是退出码,即return或者exit等函数的返回码。(这也与我们对exit返回值的探究不谋而合)
  • 如果一个进程因为异常被信号杀死,那么它的返回值将毫无意义。那么低7位会显示杀死它的信号。
  • 我们可以使用位运算来获得返回值或者杀死它的信号。
int status;
waitpid(id, &status, 0);
cout << (status >> 8) & 0xff << endl; // return value
cout << status & 0x7f << endl; //signal
  • 实际上,库函数也为我们提供了2个这样的函数:
  • WIFEXITED(status):若位正常终止子进程返回的状态,则为真。(用于查看子进程是否正常退出,即查看status的低16位的低8位是否为0.)
  • WEXITSTATUS(status):若WIFEXITED为零,提取子进程退出码。(查看子进程的退出码。即查看status的高8位。)

进程替换

  • 前面我们对fork的子进程的使用是用来分流,然后执行父进程代码的一部分,而进程替换则是让子进程去执行新的程序。
  • 所谓进程替换,像它名字一样,用一个进程去替换一个进程,而这种替换是覆盖式的替换,也会产生写时拷贝。一个进程必然有它自己的代码和数据,新的进程会完全覆盖掉旧的进程的代码和数据,但是数据结构等信息还是使用原先的,只是会重新更新信息。也就是说进程替换不会产生新的进程,它的PID不会变。
    进程替换

如图所示,新的进程会将它的数据和代码覆盖掉旧的进程,然后更新Pcb,虚拟内存和页表之间的映射关系。

exec家族

  • 进程替换的实现是通过6个函数:
    exec
  • 它们都包含在unistd库中。
  • l,即list,表示参数是以链表的形式,一个一个传入。
  • v,即vector,表示参数传入是以数组形式。
  • p,即path,带有p表示使用默认的PATH中的路径,不带p则需要自己传入路径。
  • e,即环境变量,表示自己维护环境变量。
  • path表示程序所在的路径。而file表示程序的名称。arg和argv都是是命令行参数,注意的是,需要NULL结尾。envp代表环境变量的指针数组。
// 使用execl
#include <stdio.h>
#include <unistd.h>

int main(){
	printf("begin.............\n");
	execl("/usr/bin/ls", "ls", "-a", "-l", NULL);
	printf("end...............\n");
	return 0;
}
  • execl的目的是使用ls命令来替换这个程序。
  • execl的第一个参数是ls这个命令所在的路径,据我测试,这个路径可以是绝对路径,也可以是相对路径。
  • 我想执行的是 ls -a -l,所以后面的参数形式是"ls", “-a”, “-l”。
  • 最后切记用NULL结尾,且就算你什么也不执行,后面也必须跟NULL。
  • 这段代码的结果是,执行了ls -al的命令,end没有打印。证明进程替换是覆盖式的替换。,老程序的代码被覆盖掉,自然不会打印end。
//使用更多的exec家族
#include <stdio.h>
#include <unistd.h>
int main(){
	printf("begin.............\n");
	execlp("ls", "ls", "-a", "-l", NULL);
	printf("end...............\n");
	return 0;
}
  • 带有p表示不需要显示的提供路径,它会到环境变量PATH中去寻找,你只需要提供这个程序的名字即可。
  • 两个ls,第一个是程序的名字,第二个是参数,不可缺少。
#include <stdio.h>
#include <unistd.h>
int main(){
	printf("begin.............\n");
	char *const argv[] = {
		"ls",
		"-a",
		"-l",
		NULL,
	}
	execv("/usr/bin/ls",argv);
	//execvp("ls", argv);
	printf("end...............\n");
	return 0;
}
  • 带有v实际上就是将命令行参数包含到一个指针数组中去,然后将指针数组作为参数传入。

使用自己写的程序去替换

你可能会说,难道只能用系统给的程序去替换吗?当然不。你也可以使用自己写的程序去替换自己的程序。例如:
myexe.c
mycmd.c

  • 其中mycmd.c去替换了myexe.c。
  • 这里就要说明一下execle中最后一个e的作用,它代表的意思是将环境变量传入mycmd,而据我测试,这个环境变量的替换会覆盖掉bash中的环境变量,也就是说如果你传入的env中没有定义"PWD"这个环境变量,虽然这个环境变量是全局的,mycmd中也不会接受到,因为程序替换后,它的环境变量只来自env这个数组。

进程替换与fork分流

  • 你以为进程替换就这吗?no,进程替换的强大远不在此。通过进程替换我们完成许多操作,比如使用C/C++来编写程序,然后替换成python,java写的程序!!

  • 或是使用fork分流,然后用子进程来程序替换。
    进程替换与fork分流

  • 这里程序执行的结果是ls -al,并且打印了exec success。

  • 先是fork出子进程,然后用子进程进行进程替换去执行ls命令,父进程只需要进程等待。

  • 你可能认为这没什么用处,但是bash的原理就是基于此!

  • 你只需要对父进程进行一个死循环,然后每次有命令被输入bash,bash就解析字符串,然后进行fork,进程替换,进程等待一系列的操作。

最后的怪想

  • 程序编写最有意思的一点就是自由。关于程序替换,我突然想到的怪操作:程序替换之后再替换。甚至你还可以形成一个闭环:1替换2,0替换1,2替换0,哈啊哈,我没试过,也许你们可以试试看程序崩不崩,还是会一直执行下去。
    (全文完)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值