前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【Linux】进程控制:理解什么是进程创建,进程终止,进程等待 | 进程替换

【Linux】进程控制:理解什么是进程创建,进程终止,进程等待 | 进程替换

作者头像
aosei
发布2024-01-23 15:32:40
2150
发布2024-01-23 15:32:40
举报
文章被收录于专栏:csdn-nagiYcsdn-nagiY

一.进程创建

fork函数创建进程,新进程为子进程,原进程为父进程;

fork函数包含在头文件 <unistd.h>

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

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

关于fork函数的返回值:

  • 返回0给子进程
  • 返回子进程的PID给父进程
  • 创建失败,返回值 < 0

子进程和父进程共享fork函数之后的代码

实例演示:

代码语言:javascript
复制
  int main()
  {
      printf("before: pid: %d\n",getpid());
      pid_t id=fork();
      printf("after:\n");
      if(id==0)
      {
          //子进程
          printf("我是子进程  pid: %d  ppid: %d\n",getpid(),getppid());
      }
      else if(id>0)
      {
          //父进程
          printf("我是父进程  pid:%d   ppid: %d\n",getpid(),getppid());                                                       
      }
      else
      {
          printf("出错\n");
      }
  
      return 0;
  }

二.进程终止

进程退出时的三种情况:

  • 代码运行完毕,结果正确
  • 代码运行完毕,结果不正确
  • 代码异常终止:异常退出本质是收到了对应的信号

进程退出的常用方法

  • return
  • exit
  • _exit

那么谁会关心一个进程的运行情况呢?

答案是父进程。子进程在退出时,会成为僵尸进程,需要父进程的回收。

那么父进程期望获得子进程退出时得哪些信息呢?

  • 子进程是否是异常退出
  • 没有异常;如果有,可以通过查看错误码来查看错误信息

可以通过查看退出码,来知晓进程的退出情况 

可以用以下命令查看最后一次进程退出的退出码

代码语言:javascript
复制
echo  $?

 我们可以打印一下,每个错误码对应着什么错误信息

代码语言:javascript
复制
int main()
{
     for(int i=0;i<100;i++)
     {
         printf("%d  : %s\n",i,strerror(i));
     }
  
     return 0;                                                                                                                }                                                                                                                    

可以发现,错误码为0时,代表代码正常执行完毕,所以我们平时主函数里的return 都是return 0

当然我们也可以自己设计一套错误码体系。

exit和_exit

exit 和 _exit 都可以退出进程,但是exit在退出进程前会做其它工作:

  •  执行用户通过 atexit或on_exit定义的清理函数。
  • 关闭所有打开的流,所有的缓存数据均被写入
  •  调用_exit

而 _exit 是直接退出进程,所以缓冲区绝对不在内核。

所以一般推荐使用 exit 函数来退出进程。

 return 退出

return是一种更常见的退出进程方法。执行return n等同于执行exit(n),因为调用main的运行时函数会将main的返回值当做 exit的参数。

三.进程等待

进程等待是什么?

通过系统调用wait/waitpid,来进行对子进程进行状态检测与回收的功能!

为什么要进程等待?

我们知道子进程在退出时会变成僵尸进程:

  • 僵尸进程无法被杀死,需要通过进程等待来杀掉它,进而解决内存泄漏问题---必须解决的
  • 我们要通过进程等待,获得子进程的退出情况---知道我布置给子进程的任务,它完成的怎么样了---可能关心,也可能不关心---可选的

怎么等待?

父进程通过调用 wait/waitpid 进行僵尸进程的回收问题!

wait函数

查看 man 手册 ,wait 函数所在的头文件是 <sys/types.h> 和 <sys/wait.h>

返回值:         成功返回被等待进程pid,失败返回-1。 参数:         输出型参数,获取子进程退出状态,不关心则可以设置成为NULL.

wait等待的是任意一个子进程

实例:

代码语言:javascript
复制
#include <sys/types.h>
#include <sys/wait.h> 

void Run()
{
    int cnt=5;
    while(cnt)
    {
        printf("我是一个子进程 pid: %d  ppid: %d\n",getpid(),getppid());
        cnt--;
        sleep(1);
    }
}

int main()
{
     pid_t id=fork();
     if(id==0)
     {
         //子进程
         Run();
         exit(0);
     }
     else if(id>0)
     {
         //父进程                                                                                           
         int cnt=10;
         while(cnt)
         {
            printf("我是一个父进程 pid: %d  ppid: %d\n",getpid(),getppid());
            cnt--;
            sleep(1);
         }
 
         pid_t ret=wait(NULL);
     }
     else 
         printf("出错\n");
   
     return 0;
}

可以用下面的指令查看运行时进程的变化

代码语言:javascript
复制
while :; do ps ajx | head -1 && ps ajx | grep testwait | grep -v grep;sleep 1;echo "------------------------"; done

 waitpid函数

waitpid 函数一共有三个参数

 pid: pid=-1,等待任何一个子进程。与wait等效。         pid>0,等待其进程ID与pid相等的子进程。 status:  WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出) WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码) options:   WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。

pid很容易理解,这里重点讲讲 status。

status 是一个输出型参数,它的类型是 int ,说明有32个比特位

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

低7位用来表示终止信号 第8位表示 core dump标志 第9位到第15表示退出状态,也就是说正常退出为0,异常退出为非0 所以除了上面的使用宏函数来访问status的退出码什么的还可以用下列方式访问 status 获取终止信号:status&0x7f  获取退出码:  (status>>8)&0xff

我们为什么要传一个输出型参数呢?可不可以使用全局变量代替这个输出型参数 status?

答案是不可以!因为进程之间具有独立性。

等待的原理

其实子进程在退出的时候,会把退出码,终止信号写入到PCB的 exit_code  和  exit_signal 变量中,等待进程时,也就是从子进程的PCB中读取这两个变量的值,并写入到输出型变量 status 中,这样父进程就可以知道子进程的退出信息了。

代码语言:javascript
复制
int exit_code;
int exit_signal;

阻塞等待

waitpid的第三个参数 options 为0时,表示当子进程一直没有退出的时候,父进程处于阻塞等待。

什么是阻塞等待?

即在子进程退出前,父进程什么也不做,一直在等着子进程退出,此时父进程处于阻塞状态。

非阻塞轮询

当waitpid的第三个参数 options 为 WNOHANG ,父进程以非阻塞轮询的方式等待子进程。

什么是非阻塞轮询?

即父进程会检查一次看子进程有没有退出,没有则返回0,此时父进程可以做一些自己的事,而不是一味的等待子进程的退出,在子进程退出前循环以上的过程,直到子进程退出,返回 >0 的一个数,返回负数则表示等待失败。

实例:

代码语言:javascript
复制
int main()
{
	pid_t id = fork();
	if (id == 0)
	{
		//子进程
		int cnt = 5;
		while (cnt)
		{
			printf("我是一个子进程  pid: %d  ppid: %d\n", getpid(), getppid());
			cnt--;
			sleep(1);
		}
	}
	else if (id > 0)
	{
		//父进程
		int status = 0;
		while (1)  //轮询
		{
			pid_t ret = waitpid(-1, &status, WNOHANG);  //非阻塞
			if (ret > 0)
			{
				printf("子进程退出,等待成功\n");
				break;
			}
			else if (ret == 0)
			{
				printf("你先等等,子进程还没有退出....\n");
				sleep(1);
			}
			else
			{
				printf("等待失败\n");
				break;
			}
		}

		return 0;
	}
}

四.进程替换

单进程的进程替换

在理解什么是进程替换之前,我们先来看看进程替换怎么使用,下面是操作系统提供的进程替换的一些函数

  • 这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。
  • 如果调用出错则返回-1
  • 所以exec函数只有出错的返回值而没有成功的返回值。

参数所表达的意思:

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

以上的这些函数中,只有execve是系统调用,其它函数在底层都会调用这个函数。

对于像execl 和 execlp 有可变参数的函数,其实它们的使用方法很简单,从第二个参数开始,参数的写法就很我们在命令行中的一样,且最后一个参数是NULL。

可以想想,当我们要执行一个程序时,第一件事是什么?

第一件事就是要先找到这个程序,找到程序后做什么?

第二件事就是你得知道要怎么执行这个程序。

这样就能更好的理解这些函数为什么要这么用了

例如命令行中输入 ls -l -a (以单进程的进程替换来演示)

代码语言:javascript
复制
int main()
{
     execl("/usr/bin/ls","ls","-l","-a",NULL);                                    
  
     return 0;
}

下面是其它进程替换函数的一些用法

代码语言:javascript
复制
int main()
{
     extern char**environ;
     execl("/usr/bin/ls","ls","-l","-a",NULL);
  
     char*const myargv[]={"ls","-l","-a",NULL};
     execv("/usr/bin/ls",myargv);   //v表示数组
     execvp("ls",myargv);         //有p的可以省去路径                                
     execvpe("ls",myargv,environ);   //有e的可以自己控制环境变量,且采用的策略是覆盖而不是追加
    
     return 0;
}

 进程替换的原理

先来看这样一段代码:

代码语言:javascript
复制
int main()
{
    printf("before:\n");
    execl("/usr/bin/ls","ls","-l","-a",NULL);
    printf("after:\n");

    return 0;
}

打印结果会是什么?

 发现只打印了before ,after呢?也就是 execl 前面的代码会被执行,后面的代码不会被执行,这是为什么?

进程替换的原理

进程在替换时,只会替换掉物理内存中原来程序的代码和数据,其它的并不会动,且调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。

所以execl函数执行完后,原来的代码和数据就被替换了,物理内存中是全新的代码和数据,也就不是原来的代码,所以execl后的代码不会被执行,除非execl函数调用失败。

那么环境变量也是数据,它会被替换吗?

不会!!因为创建子进程的时候,环境变量已经被子进程继承下去了,所以进程替换不会替换环境变量。

多进程的进程替换

 前面的例子是单进程的执行系统命令的进程替换,接下来我们实现一个多进程的执行自己命令的进程替换。

代码语言:javascript
复制
int main()
{


    pid_t id=fork();
    if(id==0)
    {
        execl("./other1exe","./other1exe",NULL);
    }
    else if(id>0)
    {
        wait(NULL);
        execl("./other2exe","./other2exe",NULL);
    }


    return 0;
}
多个目标的makefile文件的写法 

对于有多个目标的makefile文件,可以这样写,就可以一次生成所需要的全部文件。

代码语言:javascript
复制
.PHONY:all 
all: other1exe other2exe test 
other1exe:other1exe.cpp 
	g++ -o $@ $^ -std=c++11

other2exe:other2exe.cpp 
	g++ -o $@ $^ -std=c++11
test:test.c
	gcc -o $@ $^ -std=c99
.PHONY:clean
clean:
	rm -rf test  other1exe other2exe 

 定义一个伪目标 all ,all 的依赖文件就是你所需要生成的文件。

打印结果:


五.制作一个自己的shell 

 有了以上的这些知识后,我们就可以自己制作一个简易的myshell了,使它也能像shell那么使用。

这里要特别注意环境变量的维护!

        因为在linux中,环境变量的内容是在一个区域放着的,而环境变量表 env 中存的是环境变量的地址,这些地址指向所对应的环境变量;         而我们putenv一个环境变量时,其实是在环境变量表中找一个未使用的下标,把要导入的环境变量的地址放进去,这个地址就指向导入的环境变量的内容。         所以当我们要put环境变量时,只是将它的地址填入了环境变量表中,而环境变量的内容是由我们自己输入的,在我们自己创建的命令行参数表中,而这个命令行参数表是会变的,但环境变量表依然指向不变,但是其实所指向的内容已经变了,所以就会导致导入环境变量不成功。         为解决上述问题,我们需要自己创建空间,用来专门维护环境变量。

代码语言:javascript
复制
#define SIZE 1024
#define ARGV_SIZE 32
#define DELIM " \t"
#define EXIT_CODE -1
#define NONE -1
#define IN_RDIR 0
#define OUT_RDIR 1
#define APPEND_RDIR 2

int quit=0;
int lastcode=0;
char pwd[SIZE];
char commandline[SIZE];
char *argv[ARGV_SIZE];
char *rdirfilename=NULL;
int rdir=NONE;


//维护环境变量表
extern char**environ;
char myenv[SIZE];

const char*getusername()   //获取用户名
{
    return getenv("USER");
}


const char* gethostname()   //获取主机名
{
    return getenv("HOSTNAME");
}

void getpwd()   //获取当前路径 
{
    getcwd(pwd,sizeof(pwd));  getcwd是系统调用接口
}

void interate(char *cline,int size)  //交互
{
    getpwd();
    printf("[%s@%s %s]# ",getusername(),gethostname(),pwd);
    char*s=fgets(cline,size,stdin);
    assert(s);   //检查是否输入成功
    (void)s;  //一些编译器会对未使用的变量报警告,这里防止这个情况发生
    //abcd\n\0
    cline[strlen(cline)-1]='\0';  //将最后读入的回车变成 '\0' ,使其符合C形式的字符串
    
}


int separationline(char *cline,char*_argv[])   //分割字符串
{
    int i=0;
    _argv[i++]=strtok(cline,DELIM);   //注意strtok函数的用法
    while(_argv[i++]=strtok(NULL,DELIM));
    

    return i-1;  //返回命令行参数表的大小
}

void normalcom(int _argc,char *_argv[])   //普通命令
{
    pid_t id=fork();
    if(id==0)
    {
        execvp(_argv[0],_argv);   //通过进程替换来执行普通命令
        exit(EXIT_CODE);
    }
    else if(id>0)
    {
        int status =0;
        pid_t rid=waitpid(id,&status,0);  //等待回收子进程
        if(rid==id)
        {
            lastcode=WEXITSTATUS(status);  //设置错误码

        }
    }
    else 
    {
        perror("fork");
        return ;
    }
}

int buildcom(int _argc,char*_argv[])  //内建命令
{
    //cd  export  echo
    if(_argc==2&&strcmp(_argv[0],"cd")==0)
    {
        chdir(_argv[1]);
        getpwd();
     sprintf(getenv("PWD"),"%s",pwd);
     return 1;

    }
    else if(_argc==2&&strcmp(_argv[0],"export")==0)
    {
        strcpy(myenv,_argv[1]);
        putenv(myenv);
        return 1;
    }
    else if(_argc==2&&strcmp(_argv[0],"echo")==0)
    {
        if(strcmp(_argv[1],"$?")==0)   //打印最后一次程序退出的错误码
        {
            printf("%d\n",lastcode);
            lastcode=0;
        }
        else if(*_argv[1]=='$')  //打印环境变量
        {
            char*val=getenv(_argv[1]+1);
            if(val) printf("%s\n",val);

        }
        else   //普通打印
        {
            printf("%s\n",_argv[1]);
        }
        return 1;
    }

    if(strcmp(_argv[0],"ls")==0)   //特殊处理ls命令,为文件带上颜色,例如目录是蓝色的
    {
        _argv[_argc++]="--color";
        _argv[_argc]=NULL;
    }

    return 0;
}

int main()
{
    while(!quit)
    {
        //实现交互
        interate(commandline,sizeof(commandline));


        //分割命令
        int argc=separationline(commandline,argv);
        if(argc==0)
            continue;
        //执行命令
        
        //内建命令
        int n=buildcom(argc,argv);
        
        //普通命令
       if(!n) normalcom(argc,argv);
    }
    return 0;
}
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2024-01-23,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一.进程创建
  • 二.进程终止
    • exit和_exit
      •  return 退出
      • 三.进程等待
        • 进程等待是什么?
          • 为什么要进程等待?
            • 怎么等待?
              • wait函数
                •  waitpid函数
                  • 阻塞等待
                    • 非阻塞轮询
                    • 四.进程替换
                      • 单进程的进程替换
                        •  进程替换的原理
                          • 多进程的进程替换
                            • 多个目标的makefile文件的写法 
                        • 五.制作一个自己的shell 
                        领券
                        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档