Linux入门:进程控制

Posted 世_生

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux入门:进程控制相关的知识,希望对你有一定的参考价值。

1️⃣进程的创建

fork函数初识

在上篇文章中已经讲了fork函数,主要有:

  1. 创建子进程
  2. 有两个返回值
  3. 创建子进程成功返回0,失败返回-1
  4. 父进程返回子进程的pid

下面重点讲fork的两个返回值。

fork函数的返回值

先简单理解一下返回值0和返回值pid。
返回值0主要表示子进程的创建成功,而返回子进程的pid给父进程就是要让父进程找到这个子进程。

一个常识:“一个父亲可以有多个孩子,一个孩子只有一个父亲”。
创建子进程主要是父进程要分配任务给子进程执行,一个父进程具有多个子进程,所以,父进程需要通过子进程的pid来给子进程分配不同的任务。

那么如何理解fork函数会有两个返回值呢?

int main()
{
	printf("________");
	pid_t id=fork();	
	if(id==0)//子进程
	{;}
	else if(id>0)//父进程
	{;}
	else
	{;}
	return 0;
}

在未调用fork函数之前,只有一个进程,就是父进程。调用 fork函数过程中 return 前,子进程就创建完成了,而不是fork执行完再创建完的。

pid_t fork()
{
	//拷贝父进程,形成子进程对应的数据结构
	stack_struct
	mm_struct
	页表
	文件
	其他信息
	//os也要管理子进程,task_struct链接到进程列表中,此时子进程添加到系统的进程列表中
	
	走到这里,子进程创建完成了。
	在这里父子进程已经开始分别执行了。
	所以return 执行两次,分别被子进程和父进程。子进程执行完放回0,父进程执行完返回子进程的pid。
	
	return pid;
}

写时拷贝

一个父进程创建子进程,实则是拷贝了一份父进程数据和代码给子进程,父子代码共享。
在没有对父子进程进行写入时,父子进程的进程地址空间通过页表映射到同一块物理内存中。
当任意一方要进行写入时,os会再开辟一块内存空间给要写入的数据,这种写入的方式称之为写时拷贝


那么为什么要有写时拷贝呢?

  1. 进程具有独立性
  2. 节省空间,子进程不一定会全部使用父进程的数据,而写入的本质是需要的时候进行写时拷贝,按需分配,节省空间
  3. 延时分配,子进程要使用父进程的数据不一定现在要用,等要用的时候再分配,可以更高效的使用任何的空间

数据会写时拷贝,那么代码会不会写时拷贝?

大多数情况下不会,但是在进程的替换中会有的。

fork函数常规用法

  • 一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。

  • 一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数。

fork函数失败的原因

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

2️⃣进程的终止

我们写C语言代码时都有一个main函数,main函数是程序的入口。而在main函数结尾中都会写return 0,这个return 0到底是什么呢?return 0返回值给了谁呢?

第一个问题:return 0是返回退出码。
第二个问题:main是函数,是函数就会被人调用,其本质是操作系统调用的,return 0返回给系统。

为什么main函数有返回值?
一个程序的运行起来变成进程,来完成某种工作,一个工作的完成和未完成我们是不知道的,而main函数通过返回退出码给系统,系统来翻译退出码的意思来告诉我们是否完成工作。

echo $?//显示最近一次进程退出时的退出码

退出码是0:表示进程跑完成了,结果正确。
退出码!0:进程失败(有很多种原因)

比如我 ls 一下,然后查看ls进程的退出码。

比如我 ls 一个不存在的文件

通过下面代码来查看退出码代表的意思:

 #include<stdio.h>
 #include<string.h>
 
 int main()
 {
   int i;
   for( i=0;i<150;i++)
   {
     printf("%d: %s\\n",i, strerror(i));                                                                              
   }
   return 0;
 }

进程退出的场景

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

其中代码异常终止的情况,返回的退出码是没有任何意义的。
因为在return前就开始异常……

进程常见退出方法

正常终止(可以通过echo $?查看最近一次进程退出的退出码)

  1. 从main函数返回退出的
  2. 调用exit
  3. 调用_exit

异常退出:

  • CTRL+C,信号终止

进程终止了os做了什么?

释放掉曾经申请的数据结构,释放曾经申请的内存,从各种队列等数据结构中移除。

_exit函数

#include <unistd.h>
void _exit(int status);

参数:status 定义了进程的终止状态,父进程通过wait来获取该值。

exit函数

#include <unistd.h>
void exit(int status);

exit最后会调用_exit,但在调用之前,还做了其他工作:

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


这是因为_exit没有刷新缓冲区,没有把缓冲区的数据写入。而exit会刷新缓冲区,会把缓冲区的内容写入。

return退出

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

注意:只有main函数中的return才是进程的退出,其他函数中的return表示函数调用完毕。

3️⃣进程等待

进程等待的必要性

在上一篇博客中,讲到了僵尸进程会造成内存泄漏,但没有说僵尸进程的解决方法,而进程的等待就可以解决僵尸进程。

还有,父进程给子进程派任务完成的如何,我们是知道的。比如:子进程运行完成,结果对还是不对,或者是否正常退出。

必要性

  • 父进程通过进程等待的方式,回收子进程的资源,获取子进程的退出信息。

进程等待的方法

wait方法

#include<sys/types.h>
#include<sys/wait.h>


pid_t wait(int *status);
返回值:

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

参数:

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

代码演示:

	 #include<stdio.h>
 	 #include<unistd.h>
     #include<sys/types.h>
     #include<sys/wait.h>
      
    int main()     
    {
      pid_t id=fork();//创建子进程
      int count=5;
      while(1)//利用循环来证明子进程退出,父进程还在的情况下,子进程不会变成僵尸进程
      {
      	if(id==0)
      	{        
        	while(count > 0){
           	printf("child: pid(%d),ppid(%d)\\n",getpid(),getppid());
           	count--;
           	sleep(1);                                                                              
        	}
        _exit(11);
      	}
      	else{    
    		pid_t  id=wait(NULL);//等待子进程退出
        	printf("father: pid(%d),ppid(%d)\\n",getpid(),getppid());
      	}
      }
      return 0;
   }


获取子进程的退出信息在waitpid中讲,会了waitpid就会了wait.

waitpid方法

#include<sys/types.h>
#include<sys/wait.h>


pid_ t waitpid(pid_t pid, int *status, int options);//有三个参数

参数pid:

  • pid=-1,等待任一子进程,与wait等效。
  • pid>0,等待其进程ID与pid相等的子进程。就是说waitpid可以选择性等待子进程。

参数status:

  • status是进程的退出信息,具体的下面说。

参数options:

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

返回值:

  • 当正常返回的时候waitpid返回收集到的子进程的进程ID;
    如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
    如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;

获取子进程status

wait和waitpid,都要一个status参数,该参数是一个输出型参数,由操作系统填充。
如果传NULL,表示不关心子进程的退出信息。
否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
但是,status不是简单的当作整形来处理,而是当做位图来看待。

int型status是四个字节,32个比特位,但是退出信息只占用了低16个比特位,其中在低16个比特位中,从低8位开始到16位是来表示子进程的退出状态的,低7位表示终止信号。

看图看真相:

其中当子进程正常终止时,终止信号为0。
当子进程被信号杀死是,进程异常退出,其退出状态没有意义,而终止信号表示其退出的原因。
这样说可能不怎么理解,下面用代码来表示:

  • 当子进程正常终止,拿子进程的退出码和退出信号
    #include<stdio.h>
   #include<unistd.h>
   #include<sys/types.h>
   #include<sys/wait.h>   
   int main()   
   {
    pid_t id=fork();
   int count=5;
   if(id==0)
    {
      while(count > 0){
         printf("child: pid(%d),ppid(%d)\\n",getpid(),getppid());
         count--;
         sleep(1);
      }
      _exit(0);
    }
    int status=0;
    pid_t ret=waitpid(id,&status,0);
    if(ret >= 0)
    {
      printf("child pid:%d\\n",ret);//打印子进程的pid
      printf("status:%d\\n",status);//打印status
      printf("child exit code:%d\\n",(status>>8)&0xFF);//打印子进程的退出码
      printf("child get signal:%d\\n",status&0x7F);//打印子进程的信号
    }                                                                                 
    sleep(5);
  
    return 0;
  }

  • 当子进程异常终止,拿子进程的退出码和退出信号(当异常终止时,子进程的退出码没有任何意义)
 #include<unistd.h>
   #include<sys/types.h>
   #include<sys/wait.h>   
   int main()   
   {
    pid_t id=fork();
   int count=5;
   if(id==0)
    {
      while(count > 0){
         printf("child: pid(%d),ppid(%d)\\n",getpid(),getppid());
         count--;
         sleep(1);
      }
      _exit(0);
    }
    int status=0;
    pid_t ret=waitpid(id,&status,0);
    if(ret >= 0)
    {
      printf("child pid:%d\\n",ret);//打印子进程的pid
      printf("status:%d\\n",status);//打印status
      printf("child exit code:%d\\n",(status>>8)&0xFF);//打印子进程的退出码
      printf("child get signal:%d\\n",status&0x7F);//打印子进程的信号
    }                                                                                 
    sleep(5);
  
    return 0;
  }


上面获取子进程的退出码和退出信号只是要让你们了解status,当然还有更简单的方式来获取子进程的退出吗和推出信号。

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

子进程运行期间,父进程wait的时候做了什么?
在上面代码中,父进程什么都没有做,就是等子进程,这种行为称之为阻塞等待

什么是阻塞等待呢?
举个例子:小明在寝室楼下叫小王吃饭,小王在楼上说要等一下,之后小明在楼下等小王,在等的这个期间,小明什么事都不做,手机都不能玩,就是两个眼睛一直盯着门口等小王下来,这种方式就叫阻塞等待

WNOHANG

什么是非阻塞等待?
举个例子:小明在寝室楼下叫小王吃饭,小王在楼上说要等一下,之后小明在楼下等小王,在等的这个期间,小明接着做自己的事情,直到小王下楼和小明一起去吃饭,这种方式就叫非阻塞等待

在上面获取status的代码中,waitpid的第三个参数(options)设置成立0,这样父进程就会阻塞式等待子进程。
当options设置成WNOHANG,父进程就会非阻塞式等待子进程。

当子进程没有退出时,waitpid返回0。
当子进程退出时,waitpid返回子进程的pid。
当在调用中出错时,返回-1。

4️⃣进程程序替换

到现在我所学的fork函数创建子进程中都是执行父进程的一部分代码。没有把子进程的作用用出来,而进程程序替换就可以把子进程的作用发挥出来。

替换的原理

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

返回到上面讲到的写时拷贝,当父子进程公共的数据要被子进程写入时,就会发生写时拷贝,这是针对数据。
当子进程要调用exec函数来进程程序替换时,这时子进程的代码和数据都要替换,这也会发生写时拷贝。
保证了进程的独立性。

替换函数

有六种以exec开头的函数,统称exec函数:

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

execl来演示一下程序替换:
execl函数的参数:
path:是程序的路径
arg:要执行的命令(怎么执行)
…:后续要怎么执行的参数,最后NULL结尾

   #include<stdio.h>
   #include<unistd.h>
   #include<sys/types.h>
   #include<sys/wait.h>                                                                               
   
   int main()
   {
     pid_t id=fork();
  
    if(id==0){
      printf("child .....\\n");
      execl("/usr/bin/ls", "ls","-l","-i","-a",NULL);//替换函数
      printf("hello linux\\n");
    }
    int status=0;
    pid_t ret=waitpid(id,&status,0);
    if(ret>0)
      printf("father  ....\\n");
  
    return 0;
  
  }


execlp函数:

execlp("ls","ls","-l","-i","-a",NULL);//file(名字)

execv函数

char *const mygrav[]={"ls","-l","-i","-a",NULL};

execv("/usr/bin/ls",mygrav);

execvp函数

execlp("ls","ls","-l","-i",mygrav);//file(名字)

execle函数和execl函数没啥区别,就是后面多了个char *const envp[]
就是说execle可以自己设置环境变量。

char *const envp[] = { "MSSS=100000", NULL};

execle("ls", "ls", "-l","-i","-a", NULL, envp);//设置的环境变量会覆盖要执行的程序中的环境变量

看图看代码

函数解释

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

我们不关心exec函数的返回值。

命名理解

这些函数原型看起来很容易混,但只要掌握了规律就很好记。

  • l(list) : 表示参数采用列表
  • v(vector) : 参数用数组
  • p(path) : 有p自动搜索环境变量PATH
  • e(env) : 表示自己维护环境变量
函数名参数格式是否带路径是否使用当前环境变量
execl列表不是
execlp列表
execle列表不是不是,须自己组装环境变量
exexv数组不是
execvp数组
execve数组不是不是,须自己组装环境变量

事实上,只有execve是真正的系统调用,其它五个函数最终都调用 execve,所以execve在man手册 第2节,其它函数在
man手册第3节。这些函数之间的关系如下图所示。

下图exec函数族 一个完整的例子:

以上是关于Linux入门:进程控制的主要内容,如果未能解决你的问题,请参考以下文章

嵌入式Linux从入门到精通之第十二节:线程

Linux Supervisor的安装与使用入门

Linux Supervisor的安装与使用入门

Linux Supervisor的安装与使用入门---SuSE

SELinux入门简介

LINUX PID 1和SYSTEMD PID 0 是内核的一部分,主要用于内进换页,内核初始化的最后一步就是启动 init 进程。这个进程是系统的第一个进程,PID 为 1,又叫超级进程(代码片段