Linux系统编程05---进程与线程

作者介绍

张伟伟,男,西安工程大学电子信息学院,2019级硕士研究生,张宏伟人工智能课题组。
微信公众号:可随时查阅,搜索—张二牛的笔记,内容会分类上传。
研究方向:机器视觉与人工智能。
电子邮件:[email protected]
电子邮件:[email protected]

1.学习目标

了解进程相关的概念
掌握fork/getpid/getppid函数的使用
熟练掌握ps/kill命令的使用
熟练掌握execl/execlp函数的使用
说出什么是孤儿进程什么是僵尸进程
熟练掌握wait函数的使用 (父进程回收)
熟练掌握waitpid函数的使用(子进程回收)

2.进程的相关概念

2.1 程序与进程

程序:是编译好的二进制文件,在磁盘上,占用磁盘空间,是一个静态的概念

进程,一个启动的程序,进程占用的是系统资源,如:内存,CPU,终端等,是一个动态的概念

程序-》剧本(纸)
进程-》戏(舞台、灯光、道具)

同一个程序可以在多个终端执行,同一台戏可以在多场唱。

# 查看进程pid
ps -ef | grep 程序名

2.2 并发和并行
CPU会将一个大的时间段分成多个时间片,让进程轮流使用CPU的时间片(有个)

并发:在一个时间段内,是在同一个cpu上,同时运行多个程序。

  • 一分钟内的一杯咖啡,每个人喝一口
    在这里插入图片描述
    如:若将CPU的1S的时间分成1000个时间片,每个进程执行完一个时间片必须无条件让出CPU的使用权,这样1S中就可以执行1000个进程。
    一个时间碎片内只有一个进程在执行。
    在这里插入图片描述

并行:在一个时刻有多个程序在执行。(前提是有多个cpu

  • 一分钟内的两杯咖啡,可以两个人在同一时刻同时喝
    在这里插入图片描述

2.3 PCB-进程控制块

是一个大的结构体,每个进程在内核中都会有个进程控制块

/usr/src/linux-headers-4.4.0-96/include/linux/sched.h文件的1390行处可以查看struct task_struct 结构体定义。其内部成员有很多,我们重点掌握以下部分即可:
	进程id。系统中每个进程有唯一的id,在C语言中用pid_t类型表示,其实就是一个非负整数。
	进程的状态,有就绪、运行、挂起、停止等状态。(操作系统调度问题)
	进程切换时需要保存和恢复的一些CPU寄存器。
	描述虚拟地址空间的信息。
	描述控制终端的信息。
	当前工作目录(Current Working Directory)。
	getcwd --pwd   
    chdir 改变当前工作目录
	umask  掩码。(计算文件权限)
	==文件描述符表,包含很多指向file结构体的指针。==
	和信号相关的信息。
	id 用户id和组id。
	会话(Session)和进程组。
	进程可以使用的资源上限(Resource Limit)。
	ulimit -a

2.4 进程状态切换(面试)

进程基本的状态有5种。分别为初始态,就绪态,运行态,挂起态与终止态。其中初始态为进程准备阶段,常与就绪态结合来看。
状态切换要到运行态,必须先到就绪态(有执行资格,但是没有获得CPU的时间片),在挂起态(没有执行资格和CPU)。

在这里插入图片描述

3. 创建进程

3.1 fork函数

	函数作用:创建子进程
	原型: pid_t fork(void);
函数参数:无
返回值:调用成功:父进程返回子进程的PID,子进程返回0;
        调用失败:返回-1,设置errno值。pid_t表示一个重定义的非负整型值
●	 fork函数代码片段实例

3.2 fork的原理

  • 1. fork函数返回值
    它标识是父进程还是子进程。在父进程返回是大于0的数。(父子进程各自返回一个值,而不是返回两个值)
  • 2.父子进程的执行逻辑
    父进程执行pid > 0 的逻辑,子进程执行pid==0的逻辑
  • 3.父子进程谁先执行
    谁先抢到CPU的时间片谁执行

父进程没有可以知道子进程的函数,我们创建时可以记录,子进程可以提供相应的接口。

虚拟地址空间

内核区 PCB

用户区
环境变量
命令行参数


动态加载区
.txt
.bss
.data
受保护的区域

1. 父进程调用fork函数创建一个子进程,子进程的用户区和父进程的用户区完全一样,但是内核区不完全一样;如父进程的PID和子进程的PID不一样(先创建的pid比较小)。

3.3 fork函数的实例

//fork函数测试
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<unistd.h>

int main()
{
    
    
	//创建子进程
	//pid_t fork(void);
	pid_t pid = fork();
	if(pid<0)
	{
    
    
		perror("fork error!");
		return -1;	
	}
	else if(pid>0)
	{
    
    
		printf("father: pid == [%d]\n",getpid());
	}
	else if(pid == 0)
	{
    
    
		printf("chile: pid == [%d]  its parents pid == [%d] \n",getpid(),getppid());
	}
	return 0;
}

3.4 父进程循环创建子进程

实例:父进程循环创建n个子进程,子进程间为兄弟关系。
以下存在子进程会创建子进程的问题,然后会复制代码继续运行,复制规则从当前的 i 值开始运行,各个进程的 i 值相互独立。
在这里插入图片描述
解决方法:例如创建3个子进程,要保证父进程每次创建1个,需要在子进程中加break,而且让子进程不继续 fork 创建。

判断每次创建了几个,加if(i==1) 打印,如下代码。

//循环创建n个子进程
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>

int main()
{
    
    
	int i = 0;
	for(i=0; i<3; i++)
	{
    
    
		//创建子进程
		pid_t pid = fork();
		if(pid<0) //fork失败的情况
		{
    
    
			perror("fork error");
			return -1;
		}
		else if(pid>0)//父进程
		{
    
    
			printf("father: pid==[%d], fpid==[%d]\n", getpid(),getppid());
			//sleep(1);
		}
		else if(pid==0) //子进程
		{
    
    
			printf("child: pid==[%d], fpid==[%d]\n", getpid(), getppid());
			break;
		}
	}

	//第1个子进程
	if(i==0)
	{
    
    
		printf("[%d]--[%d]: child\n", i, getpid());	
	}

	//第2个子进程
	if(i==1)
	{
    
    
		printf("[%d]--[%d]: child\n", i, getpid());	
	}
	//第3个子进程
	if(i==2)
	{
    
    
		printf("[%d]--[%d]: child\n", i, getpid());	
	}
	//父进程
	if(i==3)
	{
    
    
		printf("[%d]--[%d]: child\n", i, getpid());	
	}
	sleep(10);

	return 0;
}

4.父子进程能否共享全局变量(不能)

写时复制,读时共享。

父子进程不能共享全局变量进行通信的。
但是如果父子进程知识对全局变量做读操作,则父子进程在内存中只有一份,属于共享。
但是如果对变量修改操作,会在内存中拷贝一个副本。

sleep(1) //调整父进程和子进程的执行顺序。

%p 打印地址。虚拟的,复制过来的所以地址一样。物理内存的地址不能访问。(mmu 内存管理单元:将虚拟内存与物理内存映射,当虚拟内存一致时,他会拷贝一份)
在这里插入图片描述

5.ps命令和kill命令

会话-----》组----》进程

ps -ef  | grep bash  //查看pid
	ps aux | grep "xxx"
	ps ajx | grep "xxx"-a:(all)当前系统所有用户的进程
	-u:查看进程所有者及其他一些信息
	-x:显示没有控制终端的进程 -- 不能与用户进行交互的进程【输入、输出】
	-j: 列出与作业控制相关的信息
	kill -l 查看系统有哪些信号
	kill -9 pid 杀死某个线程
常用的信号:SIGKILL
		  SIGTEARM
		  SIGSTOP
如下字符的含义:
//PPID 父进程  PID 自己的ID
//PGID 组ID   SID 用户身份ID  

ps命令详解.(了解不用详细掌握)

6. exec函数簇(用于子进程调用,拉起可执行程序)

应用场景:
如果想在一个进程内部执行系统命令或者是应用程序,优先应该想到如下方式:先fork(),然后在子进程里面执行execl拉起可执行程序或者命令。
原理:
在这里插入图片描述

总结:
exec函数是用一个新程序替换了当前进程的代码段、数据段、堆和栈;原有的进程空间没有发生变化,并没有创建新的进程,进程PID没有发生变化。

6.1 exec函数详解

6.1.1 execl(拉起自己写的程序)

int execl(const char *path, const char *arg, ... /* (char  *) NULL */);
参数介绍:
	path: 要执行的程序的绝对路径
	变参arg: 要执行的程序的需要的参数
	arg:占位,通常写应用程序的名字
	arg后面的: 命令的参数
	参数写完之后: NULL
返回值:若是成功,则不返回,不会再执行exec函数后面的代码;若是失败,会执行execl后面的代码,可以用perror打印错误原因。
execl函数一般执行自己写的程序。

6.1.2 execlp(PATH环境变量中的程序)

尽量起名相同,知道执行程序,exect应用程序的名字和路径最后的名字相同。

which ls 查看命令参数

函数原型: int execlp(const char *file, const char *arg, .../* (char  *) NULL */);
参数介绍:
	file: 执行命令的名字, 根据PATH环境变量来搜索该命令
	arg:占位
	arg后面的: 命令的参数
	参数写完之后: NULL
返回值:若是成功,则不返回,不会再执行exec函数后面的代码;若是失败,会执行exec后面的代码,可以用perror打印错误原因。
execlp函数一般是执行系统自带的程序或者是命令.

6.2 exec函数练习
(1)直接拉起系统指令,当然也可以直接使用execlp调用系统指令。
在这里插入图片描述
(2)自己写一个带命令行参数的代码,供其调用:
在这里插入图片描述
接着,拉起自己写的程序:
在这里插入图片描述

6.2 进程资源回收(为什么要进行进程资源回收)

当一个进程退出之后,进程能够回收自己的用户区的资源,但是不能回收内核空间的PCB资源必须由它的父进程调用wait或者waitpid函数完成对子进程的回收,避免造成系统资源的浪费。

6.3孤儿进程(父进程死了,子进程还活着)

若子进程的父进程已经死掉,而子进程还存活着,这个进程就成了孤儿进程。
为了保证每个进程都有一个父进程,孤儿进程会被init进程(1号进程)领养,init进程成为了孤儿进程的养父进程,当孤儿进程退出之后,由init进程完成对孤儿进程的回收。

6.4僵尸进程(子进程死掉了,父进程还活着,但是父进程没有完成回收)

解决:不接收信号—杀死其父进程–让init进程领养僵尸进程,由init进程回收僵尸进程。

 僵尸进程的概念:
若子进程死了,父进程还活着, 但是父进程没有调用wait或waitpid函数完成对子进程的回收,则该子进程就成了僵尸进程。

6.4.1 如何解决僵尸进程

wait函数

#include<sys/types.h>
#include<sys/wait.h>
	wait函数(注意,只在父进程中使用,阻塞等待,等子进程运行完后回收)
	函数原型:我们定义一个status整型变量 int status,然后指针传入。
pid_t wait(int *status);
	函数作用:
	阻塞并等待子进程退出 
	回收子进程残留资源 
	获取子进程结束状态(退出原因)。
	返回值:
	成功:清理掉的子进程ID;
	失败:-1 (没有子进程)
	status参数:子进程的退出状态 -- 传出参数
	WIFEXITED(status):为非0        → 进程正常结束
	WEXITSTATUS(status):获取进程退出状态 子进程return的参数例如return 9WIFSIGNALED(status):为非0 → 进程异常终止
	WTERMSIG(status):取得进程终止的信号编号。

在这里插入图片描述
waitpid函数

	函数原型:
pid_t waitpid(pid_t pid, int *status, in options);
	函数作用
	同wait函数
	函数参数
参数:
pid:
	pid = -1 等待任一子进程。与wait等效。
	pid > 0 等待其进程ID与pid相等的子进程。
	pid = 0 等待进程组ID与目前进程相同的任何子进程,也就是说任何和调用
	waitpid()函数的进程在同一个进程组的进程。
	pid < -1 等待其组ID等于pid的绝对值的任一子进程。(适用于子进程在其他组的情况)
status: 子进程的退出状态,用法同wait函数。
options:设置为WNOHANG,函数非阻塞,设置为0,函数阻塞。
	 函数返回值
	>0:返回回收掉的子进程ID;
	-1:无子进程
	=0:设置为0则为阻塞 ,设置为WNOHANG,设置为非阻塞,且子进程正在运行。

写入循环,保证父进程晚于子进程退出,避免僵尸进程。就是父进程还活着,或者运行过了回收的指令,子进程的状态根据wpid判断比较合理。
在这里插入图片描述
调用一次waitpid或者wait函数每次只能回收一个子进程。

后续也可以使用信号对进程进行回收。

猜你喜欢

转载自blog.csdn.net/qq_41858510/article/details/121989154
今日推荐