Linux进程控制(含进程程序替换)

目录

进程创建

fork函数的认识

fork函数返回值

返回值的解析

为什么fork拥有两个返回值?

写时拷贝

fork常规用法

fork调用失败的原因

进程终止

进程常见退出的场景

进程退出码

​编辑

_exit函数与exit函数

 进程异常退出

进程等待

为何要有进程等待

进程等待的方法

见见猪跑

status

代码示例

wait方法

waitpid方法 

非阻塞等待与阻塞等待

进程程序替换

替换原理

见见猪跑

替换函数

​编辑

1、execl

2、execlp

3、execle

4、execv

5、execvp

6、execve

命名理解

做一个简易的shell

完整代码

部分特殊指令解析

cd命令

动画演示


进程创建

fork函数的认识

在linux中fork函数是非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。

fork的返回值:

子进程中返回0,父进程返回子进程pid,出错返回-1

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

  • 分配新的内存块和内核数据结构给子进程
  • 将父进程部分数据结构内容拷贝至子进程
  • 添加子进程到系统进程列表当中
  • fork返回,开始调度器调度

如一下代码:

 运行结果:

 我们可以发现,Before输出了一次,但是调用fork()函数之后After输出了两次,也就是说调用fork()之后父子两个执行流分别执行。至于谁先执行完全由调度器决定。

fork函数返回值

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

返回值的解析

为什么给父进程返回子进程的PID?

因为父进程创建子进程的目的是让其执行任务的,父进程只有知道了子进程的PID才能很好的对该子进程指派任务。

子进程的创建

其实子进程的创建是在return语句之前就被创建好的,fork函数内部需要创建子进程的进程控制块、创建子进程的进程地址空间、创建子进程对应的页表。

为什么fork拥有两个返回值?

在一个函数中当我们return的时候说明这个函数中的各种操作已经完成,说明在fork函数返回之前,就已经有了父子两个进程,给父进程返回子进程的PID,给子进程返回0,失败则返回-1.因此父子进程分别返回不同的值,我们就可以利用这特性来根据返回值不同来区分父子进程。

写时拷贝

  

通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自形成一份副本。

父子进程的代码是共享的,即父子进程的代码和数据通过页表映射到物理内存的同一块空间。只有当父进程或子进程需要修改数据时,才将父进程的数据在内存当中拷贝一份,然后再进行修改。

fork常规用法

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

fork调用失败的原因

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

进程终止

进程常见退出的场景

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

进程退出码

函数运行结束后之后就会返回一个进程退出码,例如我们常用的main函数总是在函数的最后返回0这个操作。其实是非常必要的。当返回的值为0时我们认为进程正常退出,然而非0时就认为是进程异常退出。

当我们的代码运行起来就变成了进程,当进程结束后main函数的返回值实际上就是该进程的进程退出码,我们可以使用echo $?命令查看最近一次进程退出的退出码信息。

查看退出码

例如这个代码: 

 如下执行情况:

可知退出码为0。

因此也应证了我们上述说的退出码为0的时候程序是正常运行退出的。

退出码非0的各种情况

我们知道,进程执行错误的话原因肯定是多种的,那么如果我们使用echo ¥?算出的退出码为别的数字那么又代表什么呢?

代码如下:

 执行如下:

因此我们就可以看到各种退出码所对应的错误信息。

_exit函数与exit函数

exit函数

如下代码:

 执行结果:

  _exit

如下代码:

 执行结果:

 因此我们可以发现虽然_exit函数和exit函数都可以终止程序,但是_exit不会对程序结束的时候对之前的程序做收尾工作,而是直接终止,exit会对之前的程序做收尾工作,也就是输出之前程序需要输出的值。

 关系图如下:

我们在查看指令手册的时候发现:

exit是库函数。 

_exit是系统调用函数。 

我们又知道系统调用与用户使用的库函数在操作系统层面的分布情况:

因此大概可以确定缓冲区的位置。 

 进程异常退出

1、使用kill -9使程序异常退出

2、代码错误导致程序退出

进程等待

为何要有进程等待

  1. 之前讲子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。
  2. 进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法 杀死一个已经死去的进程。
  3. 父进程派给子进程的任务完成的如何,我们需要知道。
  4. 子进程运行完成,结果对还是不对, 或者是否正常退出。 父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息

进程等待的方法

见见猪跑

#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>

int main()
{
	pid_t id = fork();
	if (id == 0)
	{
		//child
		int cnt = 10;
		while (cnt)
		{
			printf("我是子进程: %d, 父进程: %d, cnt: %d\n", getpid(), getppid(), cnt--);
			sleep(1);
		}
		exit(0);//进程退出                                                     
	}
	sleep(15);
	pid_t ret = wait(NULL);

	if (id > 0)
	{
		printf("wait success: %d\n", ret);
	}
	sleep(5);

	return 0;
}

解析:前十秒子进程在进行pid和ppid的打印,到十秒后子进程就退出了。但是此时父进程还在sleep,因此无法回收子进程,因此此时(也就是十一秒到十五秒之间)子进程就变成了僵尸进程,十五秒后,父进程醒来开始回收子进程,子进程就被回收了。 然后最后那个sleep(5)是为了让我们很好的观察此时只有父进程了。

status

下面进程等待所使用的两个函数wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统进行填充。
如果对status参数传入NULL,表示不关心子进程的退出状态信息。否则,操作系统会通过该参数,将子进程的退出信息反馈给父进程。

方法如下:

常规方法:

exitCode = (status >> 8) & 0xFF; //获取退出码
exitSignal = status & 0x7F;      //获取退出信号

利用宏来计算的方法:

exitNormal = WIFEXITED(status);  //获取退出信号
exitCode = WEXITSTATUS(status);  //获取退出码

退出码:如果异常退出可以很好的检测到是有什么原因导致的异常。

退出信号:用来判断是否正常退出。

由此可得,当一个进程非正常退出时,说明该进程是被信号所杀,那么该进程的退出码也就没有意义了。前两种是由于status不同比特位代表的信息不同所以需要进行位运算来获取我们需要的比特位进而来判断对应的信息,由于使用的并不多这里就不做解释。 

注意:

我们去获取子进程的退出码和退出信号的时候,如果退出信号不为0,说明此时是异常退出,此时的退出码就没有意义了。

代码示例


#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>

int main()
{
	pid_t id = fork();
	if (id == 0)
	{
		//child
		int cnt = 5;
		while (cnt)
		{
			printf("我是子进程: %d, 父进程: %d, cnt: %d\n", getpid(), getppid(), cnt--);
			sleep(1);
		}
		// 1、代码跑完,结果对                                            
		// 2、代码跑完,结果不对
		// 异常情况下
		// 3、代码没跑完,结果不正确
		exit(10);//进程退出
	}

	//parent
   // pid_t ret =wait(NULL);
	int status = 0;// 不是被整体使用的,有自己的位图结构
	pid_t ret = waitpid(id, &status, 0);
	if (id > 0)
	{
		printf("wait success: %d, sig number: %d,child exit code: %d\n", re    t, (status & 0x7F), (status >> 8 & 0xFF));
	}
	sleep(5);

	return 0;
}

 

wait方法

头文件:

#include<sys/types.h>

 #include<sys/wait.h>

函数原型:

 pid_t wait(int*status);

返回值:

成功返回被等待进程pid,失败返回-1。

参数:

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

  1 #include<stdio.h>                                                                         
  2 #include<string.h>
  3 #include<stdlib.h>
  4 #include<sys/types.h>
  5 #include<sys/wait.h>
  6 #include<unistd.h>
  7 int main()
  8 {
  9     pid_t id=fork();
 10     if(id==0)
 11     {
 12       //child
 13       int cnt=3;
 14       while(cnt)
 15       {
 16         printf("child[%d] is running :cnt is :%d\n",getpid(),cnt);
 17         cnt--;
 18         sleep(1);
 19       } 
 20       exit(0);
 21     }
 22     
 23     int status=0;
 24     pid_t ret=wait(&status);
 25     if(ret>0)
 26       //wait success
 27     {
 28       printf("wait child success...\n");
 29       if(WIFEXITED(status))
 30       {
 31         printf("exit code: %d\n",WIFEXITED(status));
 32       }
 33     }
 34     sleep(3);
 35     return 0;
 36     }

可见父进程等到了子进程,并且返回了子进程的退出码。

waitpid方法 

函数原型:

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: 

通过输出型参数进行获取status :



//我们首先用一个简单的输出型参数来让我们了解
//一下什么是输出型参数,输出型参数又是如何获取参数的值的。
void add(int a, int b, int* result)
{
	*result = a + b;
}
int main()
{
	int a = 2, b = 3;
	int sum = 0;
	add(a, b, &sum);
	printf("%d\n", sum);
	return 0;
}


int status = 0;
pid_ t waitpid(pid_t pid, int* status, int options)
{
	//该函数可以通过进程id来获取status并且计算status
	*status = (这里面是计算status所使用的公式);
}
//然后就可以得到我们对应的status了

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

WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)

options:

WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进 程的ID。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
int main()
{
	pid_t id = fork();
	if (id == 0){
		//child          
		int count = 10;
		while (count--){
			printf("I am child...PID:%d, PPID:%d\n", getpid(), getppid());
			sleep(1);
		}
		exit(0);
	}
	//father           
	int status = 0;
	pid_t ret = waitpid(id, &status, 0);
	if (ret >= 0){
		//wait success                    
		printf("wait child success...\n");
		if (WIFEXITED(status)){
			//exit normal                                 
			printf("exit code:%d\n", WEXITSTATUS(status));
		}
		else{
			//signal killed                              
			printf("killed by siganl %d\n", status & 0x7F);
		}
	}
    printf("wait child failed\n");
	sleep(3);
	return 0;
}

执行结果:

其实单独看执行结果我们发现wait与waitpid没什么区别,其实如果我们对waitpid传入的第一个参数不是子进程的pid的话那么就会等待失败。

如下:

pid_t ret=waitpid(id+1,&status,0);//等待一个指定的进程

此时第一个参数已经不是子进程的pid了。

 因此我们可以发现,此时输出等待子进程失败。

非阻塞等待与阻塞等待

以上我们学过的例子中我们会发现,只要是父进程等待子进程的例子。父进程在等待的过程不能做任何的事情,这种等待叫做阻塞等待。

实际上我们可以让父进程不要一直等待子进程,而是在子进程未退出的时候父进程可以做自己的事情,如果子进程退出那么父进程再读取子进程的退出信息。这种叫做非阻塞等待。

WNOHANG

WNOHANG作为waitpid的第三个参数,当然也是利用它可以达到非阻塞等待。

当我们给第三个参数传入WNOHANG的时候,这样父进程就不会一昧什么都不做的去进行等待,而是隔一段时间去检测是否子进程退出,如果没有退出waitpid就会返回0,那么父进程就可以做自己的事情,如果退出就会返回子进程的pid。

代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
	pid_t id = fork();
	if (id == 0){
		//child
		int count = 3;
		while (count--){
			printf("child do something...PID:%d, PPID:%d\n", getpid(), getppid());
			sleep(3);
		}
		exit(0);
	}
	//father
	while (1){
		int status = 0;
		pid_t ret = waitpid(id, &status, WNOHANG);
		if (ret > 0){
			printf("wait child success...\n");
			printf("exit code:%d\n", WEXITSTATUS(status));
			break;
		}
		else if (ret == 0){
			printf("father do other things...\n");
			sleep(1);
		}
		else{
			printf("waitpid error...\n");
			break;
		}
	}
	return 0;
}

 执行结果:

运行结果就是,父进程每隔一段时间就去查看子进程是否退出,若未退出,则父进程先去忙自己的事情,过一段时间再来查看,直到子进程退出后读取子进程的退出信息。

进程程序替换

替换原理

用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数 以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动 例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。

我们在进行进程程序替换的时候,是子进程进行替换的。因此肯定需要创建子进程,我们在创建子进程的时候需要给子进程创建对应的PCB(task_struct)和进程地址空间(虚拟地址空间)和页表。接下来进行程序替换的时候,为保证进程的独立性,是需要给子进程重新拷贝一份儿物理内存的,然后对应的页表重新映射进新的物理内存内,这个称为写时拷贝,因此不仅代码可能发生写时拷贝,数据也会发生写时拷贝。如下图所示:

简而言之:

进程并不改变,但是替换其中的数据和代码来实现进程替换。

见见猪跑

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>


int main()
{
  // .c -> exe -> load -> process -> 运行 -> 执行我们现在所写的代码
  printf("process is running...\n");
  
  // load -> exe
  // 只要是一个函数,调用就有可能失败,就是没有替换成功,就是没有替换
  // exec* 为什么没有成功返回值呢?因为成功了,就和接下来的代码无关了,判断毫无意义
  // execl 只要返回了,一定是错误了 
  //execl("/usr/bin/sdafasdf"/*要执行哪一个程序*/,"ls","--color=auto","-a","-l",NULL/*你想怎么执行*/);// 所有的exec* end of NULL
  
  //perror("execl");
 

  execl("/usr/bin/ls"/*要执行哪一个程序*/,"ls","--color=auto","-a","-l",NULL/*你想怎么执行*/);// 所有的exec* end of NULL
  execl("/usr/bin/top"/*要执行哪一个程序*/,"top",NULL/*你想怎么执行*/);// 所有的exec* end of NULL


  //为什么这里的printf没有再执行了???
  //printf也是代码,是在execl之后的,execl执行完毕的时候代码已经被全部覆盖
  //开始执行新的代码程序了,所以printf无法执行了!
  printf("process running done...\n");



  return 0;
}

 通过结果我们可以发现,此时我们execl下边的printf代码并没有进行打印,原因是,在我们执行execl的时候已经发生了程序替换,后边的程序已经被替换,所以将不会执行execl后边的代码。

所展示代码中也有替换失败的例子(替换一个本不存在的),我们可以发现此时原来的程序照样执行。execl的函数只有错误的返回值,并没有成功的返回值,原因是成功之后就与后边的代码没有关系了,返回值也没有意义了,因此只有返回错误的返回值。

替换函数

1、execl

int execl(const char *path,const char *arg,...)

第一个参数是我们要执行的路径,第二个参数是可变参数列表,表示我们想如何执行这个程序,第三个参数现在我们不需要管传NULL就行。

举例:

execl("/usr/bin/ls", "ls", "-a", "-i", "-l", NULL);

2、execlp

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

第一个参数是要执行程序的名字,第二个参数是可变参数列表,表示你要如何执行这个程序,并以NULL结尾。

举例:

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

3、execle

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

第一个参数是要执行程序的路径,第二个参数是可变参数列表,表示你要如何执行这个程序,并以NULL结尾,第三个参数是你自己设置的环境变量。

例如,你设置了MYVAL环境变量,在mycmd程序内部就可以使用该环境变量。

char* myenvp[] = { "MYVAL=2021", NULL };
execle("./mycmd", "mycmd", NULL, myenvp);

4、execv

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

第一个参数是要执行程序的路径,第二个参数是一个指针数组,数组当中的内容表示你要如何执行这个程序,数组以NULL结尾。

例如,要执行的是ls程序。

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

5、execvp

int execvp(const char *file, char *const argv[]);

第一个参数是要执行程序的名字,第二个参数是一个指针数组,数组当中的内容表示你要如何执行这个程序,数组以NULL结尾。

例如,要执行的是ls程序。

char* myargv[] = { "ls", "-a", "-i", "-l", NULL };
execvp("ls", myargv);

6、execve

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

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

例如,你设置了MYVAL环境变量,在mycmd程序内部就可以使用该环境变量。

char* myargv[] = { "mycmd", NULL };
char* myenvp[] = { "MYVAL=2021", NULL };
execve("./mycmd", myargv, myenvp);

命名理解

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

做一个简易的shell

步骤如下:

1、打印提示符

2、获取命令字符串

3、解析命令字符串

4、检测命令是否需要shell本身执行的,内建命令

5、执行第三方命令

完整代码

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <assert.h>
#include <string.h>

#define NUM 1024
#define OPT_NUM 64

char lineCommand[NUM];
char *myargv[OPT_NUM];
int lastCode=0;
int lastSig=0;


int main()
{
  while (1)
  {
    // 输出提示符
    printf("用户名@主机名 当前路径# ");
    //刷新缓冲区,将printf中打印的值给立即输出出来
    fflush(stdout);




     接受用户输入的字符串
    char *s = fgets(lineCommand, sizeof(lineCommand) - 1, stdin);
    assert(s != NULL);

    (void)s;//为使shell不报错,进行强转去掉也无妨




     将我们输入的字符串进行分割

    // 目的是将我们输入指令后敲的那个回车给去掉
    lineCommand[strlen(lineCommand) - 1] = 0;
    // printf("test: %s\n",lineCommand);

    //"ls -a -l -i" -> "ls" "-a" "-l" "-i" -> l -> n
    // 字符串切割
    myargv[0] = strtok(lineCommand, " ");




    int i=1;
     判断是否为ls命令,如果为ls命令 加上颜色
    if(myargv[0]!=NULL&&strcmp(myargv[0],"ls")==0)
    {
      myargv[i++]=(char*)"--color=auto";
    }
    
    // 如果没有子串了,strtok->NULL,myargv[end]=NULL
    //将切割后的子串一个个赋值给myargv 当切割结束刚好返回NULL
    //while判断条件中先赋值再判断
    while (myargv[i++] = strtok(NULL, " "));





     如果是cd命令,不需要创建子进程,让shell自己执行对应的命令
    //本质就是执行系统接口  
    //像这种不需要让我们子进程来执行,而是让shell自己执行的命令 --- 内建/内置命令
    if(myargv[0]!=NULL&&strcmp(myargv[0],"cd")==0)
    {
      if(myargv[1]!= NULL) chdir(myargv[1]);//chdir可以直接改变当前进程目录
      continue;//直接退出此次while循环,已经不需要再使用子进程了
    }



    if(myargv[0] != NULL && myargv[1] != NULL && strcmp(myargv[0],"echo")==0)
    {
      if(strcmp(myargv[1],"$?")==0)
      {
        printf("%d, %d\n",lastCode,lastSig);
      }
      else 
      {
        printf("%s\n",myargv[1]);
      }
      continue;
    }



     测试是否成功,条件编译(只是对我们前期简写代码的一个测试)
#ifdef DEBUG
    for (int i = 0; myargv[i]; i++)
    {
      printf("myargv[%d]: %s\n", i, myargv[i]);
    }
#endif


    // 执行命令
    // 使用fork创建子进程来完成我们需要完成的进程程序替换
    pid_t id=fork();
    //如果id==-1 直接终止程序
    assert(id!=-1);

    // 如果id==0则说明是子进程 接下来进行子进程内的进程程序替换
    if(id==0)
    {
        execvp(myargv[0],myargv);
        //替换后退出,让父进程能接受到退出码
        exit(1);
    }
    
    // 父进程在此等待子进程的退出,回收子进程的退出信息
    // 以防没有回收子进程造成僵尸进程
    int status =0;
    pid_t ret =waitpid(id,&status,0);
    assert(ret>0);
    (void)ret;
    // 我们把退出码和退出信号保存在这两个变量中以便于一会儿我们打印
    lastCode=((status>>8)&0xFF);
    lastSig=(status&0x7F);





    
  }
}

部分特殊指令解析

cd命令

首先我们要知道cd命令是返回上一级的目录,我们首先查看当前目录。

#include<stdio.h>
#include<unistd.h>


int main()
{
  while(1)
  {
    printf("我是一个进程: %d\n",getpid());
    sleep(1);
  }
}

所用命令: ls  /proc/进程id  -al

 chdir是可以改变当前进程工作目录的:

#include<stdio.h>
#include<unistd.h>


int main()
{
  chdir("/home/mwb");
  while(1)
  {
    printf("我是一个进程: %d\n",getpid());
    sleep(1);
  }
}

因此我们可以使用chdir,使我们自己实现的shell中进行cd命令的实现。在对该进程工作目录进行改变的时候,像这种不需要让我们子进程来执行,而是让shell自己执行的命令称之为 ---内建/内置命令。

动画演示

 

 

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

袁百万

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值