linux--进程控制


往期相关文章--------》 深入理解进程

进程创建

系统调用 fork

从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。

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

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

fork函数返回值

#include <unistd.h>  //头文件
 pid_t fork(void);   //格式
 //返回值:成功时子进程中返回0,父进程返回子进程id,出错返回-1

fork测试
当一个进程调用fork之后,就有两个二进制代码相同的进程。而且它们都运行到相同的地方。但每个进程都将可以开始它们自己的旅程,是独立的。

int main()
{
	pid_t pid;
	printf("forkBefore: pid is %d\n", getpid()); //没有fork创建子进程之前
	if ((pid=fork()) == -1 )
	{
		perror("fork()");
		exit(1);  //进程退出函数
	}
	printf("After:pid is %d, fork return %d\n", getpid(), pid); //fork之后
	sleep(1);
	return 0;
}

程序执行结果如下:

forkBefore: pid is 20987   //fork之前先打印一次 
forkAfter:pid is 20987, fork return 20988  //父进程打印,fork成功返回的是子进程的id    
forkAfter:pid is 20988, fork return 0    //子进程打印    返回pid为 0 

这里看到了三行输出,一行forkBefore,两行forkAfter。进程20987 先打印before消息,然后它有打印after。另一个after消息有20988打印的。注意到进程20988没有打印before。
分析如下:
由于进程控制块里保留了上下文信息和程序计数器,故fork之后子进程从fork系统调用的下一条语句开始执行,故不执行一开始的那条before语句。
fork之前父进程独立执行,fork之后,父子两个执行流分别执行。注意,fork之后,谁先执行完全由调度器决定。
在这里插入图片描述
写时拷贝
通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本。
在这里插入图片描述
总结来讲:父子进程代码共享,数据各自开辟(虚拟地址)空间,私有一份,但是一旦不是只读,而对数据修改时,系统就会就会给其另外开辟一个物理内存空间,然后更新映射页表。

fork常规用法
一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。
一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数。
在这里插入图片描述

进程终止

进程退出场景
1.代码运行完毕,结果正确
2.代码运行完毕,结果不正确
3.代码异常终止

进程常见退出方法
正常终止:
1 从main里的return语句返回(return是一种更常见的退出进程方法。执行return n等同于执行exit(n),因为调用main的运行时函数会将main的返回值当做 exit的参数。)
2. 调用exit (库函数,其实实现底层封装了_exit)
3. _exit(系统调用)

异常退出:
1.ctrl + c,信号终止
2.程序崩溃:空指针访问、内存访问越界等

_exit函数(系统调用)

#include <unistd.h>
void _exit(int status);
参数:status 定义了进程的终止状态,父进程通过wait函数来获取该值
说明:虽然status是int,但是仅有低8位可以被父进程所用。所以_exit(-1)时返回值255。

exit函数

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

exit底层也会调用exit, 但在调用exit之前,还做了其他工作:
1.执行用户通过 atexit或on_exit定义的清理函数。
2. 关闭所有打开的流,所有的缓存数据均被写入
3. 调用_exit
在这里插入图片描述
刷新缓冲区(一块内存)的几种常见的方式:
1.main函数的return语句
2.fflush(stdout) 强制刷新缓冲区
3.缓冲区满的情况
4.\n 换行符
5. 调用exit 函数也会刷新缓冲

进程等待

进程等待必要性

之前讲过,子进程退出,父进程如果接收不到返回信息,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。另外,进程一旦变成僵尸状态,的kill -9 也无法结束掉次进程,因为谁也没有办法杀死一个已经(僵死)死去的进程。
最后,父进程派给子进程的任务完成的如何,我们也需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息
进程等待的方法

wait方法

#include<sys/types.h>
#include<sys/wait.h>
   pid_t wait(int*status);
返回值:成功返回被等待进程pid,失败返回-1。
参数:输出型参数,获取子进程退出状态,不关心则可以设置成为NULL

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:
     WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
     WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
  options:
  	WNOHANG(非阻塞): 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。
  	0(阻塞):子进程没有结束的话,父进程会一直等待而不继续执行自己的逻辑

另外:

	wait() == waitpid(-1,NULL,0)    这两者等价  
	说明:调用wait会导致父进程进入阻塞状态,直到有一个子进程(任一个)退出后,才执行wait逻辑然后继续。

注意:
如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息。
如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。
如果不存在该子进程,则立即出错返回。

获取子进程status

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

测试代码:
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
int main( void )
{
	pid_t pid;
	if ( (pid=fork()) == -1 ){
		perror("fork");
		exit(1);
		}
	if ( pid == 0 ){
		sleep(20);
		exit(10); //子进程退出信息返回给父进程,通过位图&运算,得出退出码
	} 
	else {
		int st;
		int ret = wait(&st);  接收 退出信息状态,不是退出码,退出码要用位图运算的
		if ( ret > 0 && ( st & 0X7F ) == 0 ){ // 说明第低七位全为0,正常退出
			printf("child exit code:%d\n", (st>>8)&0XFF); //&运算,输出退出码
		} 
		else if( ret > 0 ) {   //(st & 0X7F ) > 0 说明第七位里有值,异常退出
			printf("sig code : %d\n", st&0X7F ); //&运算 输出退出码
		}
	}
}

进程程序替换

替换原理

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

替换函数簇

可以使用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[]);

参数解释:
path:带路径的可执行程序
file:可执行程序名 也可以带路径
arg:可变参数列表(给可执行的程度传递的参数选项等)
	注意第一个参数为 可执行程序名
argv[]:指针数组,存储的要替换的命令行参数 第一个参数也是可执行程序名,最后以NULL结尾
envp[]:需要自己组织的环境变量,若不组织,环境变量为空	

函数解释
1.这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。
2.如果调用出错则返回-1,所以exec函数只有出错的返回值而没有成功的返回值,因为替换成功后就去执行别的程序去了。

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

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

调用举例

#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);
	//带v的,参数直接传指针数组
	execv("/bin/ps", argv);
	// 带p的,可以使用环境变量PATH,无需写全路径
	execvp("ps", argv);
	// 带e的,需要自己组装环境变量
	execve("/bin/ps", argv, envp);
	exit(0);
}

关系:只有execve是系统调用,其它五个函数最终都调用 execve。
在这里插入图片描述

进程控制实例–简单shell的实现

可以先去看-----》shell以及运行原理解释
在这里插入图片描述
所以要写一个shell,需要循环以下过程:

  1. 获取命令行 :父进程从标准输入获取用户输入
  2. 解析命令行 :再分割出命令名字和之后的命令参数
  3. 建立一个子进程(fork)
  4. 替换子进程(execvp) :替换的函数配置好后去内核调用执行传来的参数命令
  5. 父进程等待子进程退出(wait)

学习到进程程序替换, 写一个简单的微型shell,重新认识shell运行原理

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<fcntl.h>
#include<ctype.h>
#include<sys/wait.h>
#define MAXSIZE 1024

char command[MAXSIZE];//存储命令

int Gitcommand()   //输入读取命令行
{
  memset(command,0,sizeof(command)); //初始化一块内存
  printf("[my minishall]$ ");
  fflush(stdout);  //刷新缓冲区
  if (fgets(command,MAXSIZE-1,stdin)==NULL) //从标准输入里获取用户输入的命令
 {
    printf("fgets error");
    return -1;
 }
  return 0;
}
//命令可能带有参数 需要分割 命令名与参数选项名 比如:ls -a -l
int dealcommand(char *command) //分割
{
  if(*command=='\0') //检查输入的有效性
  {
    printf("command NULL error");
    return -1;
  }
  int argc=0; //参数个数
  char *argv[MAXSIZE]={0}; //参数列表
  while(*command)  
  {
    if(!isspace(*command))   //可以省去前面没用的空格 
    {
      argv[argc]=command;  //先把头部保留进去
      argc++;
    
      while(!isspace(*command)&& *command!='\0')//直到遇到下一个空格,一块命令就分出来了     {
        command++;
      }
    }
    *command='\0'; //添上结尾达到分割
    command++; //继续分割下一块
  }
  argv[argc]=NULL;  //参数数组最后以NULL结尾,不要忘记
  execcommand(argv); //把分割好的参数传给进程替换函数
  return 0;
}
int execcommand(char *argv[])
{
  if(argv[0]==NULL)
  {
    printf("exec error");
    return -1;
  }
  pid_t pid=fork();  //创建进程
  if(pid<0) 
  {
    perror("fork error");
    return -1;
  }
  else if(pid==0)
  {
    execvp(argv[0],argv);//子进程程序替换,去执行命令
    exit(0);
  }
  else 
  {
    waitpid(pid,NULL,0);//父进程 等待,防止子进程僵尸进程
  }
  return 0;
}
int main()
{
  while(1)  //循环输入
  {
    if(Gitcommand()==-1)
    {
      continue;//继续获取命令
    }
    //获取完成后处理命令
    dealcommand(command);
  }
  return 0;
}

简单测试
在这里插入图片描述

发布了44 篇原创文章 · 获赞 88 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/qq_44785014/article/details/104794187