【linux应用编程】-进程全解

第一点

程序的开始和结束

程序的开始

在操作系统中的应用程序其实在main函数执行前也是需要先执行一段引导代码的,但是我们在写代码之前完全不用考虑引导代码的问题,因为这些都由内部封装完成!

上述的这些步骤,拆开的讲就是编译和链接,这两个步骤 由链接器完成,得到我们使用的a.out

然后下一步骤再由加载器(操作系统中的程序)负责将这个程序加载到内存中去执行这个程序!

程序的结束

程序结束分为两种,一种是正常终止(return、exit、_exit) 另一种是非正常终止(自己或他人发信号终止进程)

atexit注册进程终止处理函数

对atexit注册进程终止处理函数的理解:如果没有这个函数,当程序终止时,我们所执行的程序(函数)就完全不用再去执行,执行到哪也不管就直接退出,相当于正常上班时地震了,员工完全无暇顾忌其他,逃命要紧,也不会去整理今天的工作成果,但是如果有这个函数,他会去注册现有的执行的函数,他的功能就好像员工到点下班,一旦时间到,员工也会整理桌面,一切收拾妥当再回家。示例如下:并且atexit函数是先注册后执行,后注册先执行

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

void func1(void)
{
	printf("func1\n");
}

void func2(void)
{
	printf("func2\n");
}


int main(void)
{
	printf("hello world.\n");
	
	// 当进程被正常终止时,系统会自动调用这里注册的func1执行
	atexit(func2);     //@ 7 步骤1
	atexit(func1);		//@ 7 步骤2
	
	printf("my name is lilei hanmeimei\n");     //@7 步骤3
	
	
	//return 0;
	exit(0);//@7 步骤4   @7 步骤5 执行func1   @7 步骤6 执行func2  先进后出
	_exit(0);//@7不执行进程注册函数
	
}

return、exit和_exit的区别:return和exit效果一样,都是会执行进程终止处理函数,但是用_exit终止进程时并不执行atexit注册的进程终止处理函数。

第二点

进程环境

环境变量

环境变量就相当于我们平时程序中的全局变量,他是linux内核中的一种已经定义好的变量,当我们进程中需要的的时候就可以直接声明调用;

可以用export这个命令去查看环境变量

程环境表介绍:每一个进程中都有一份所有环境变量构成的一个表格,也就是说我们当前进程中可以直接使用这些环境变量。在程序中通过environ全局变量使用环境变量。进程环境表其实是一个字符串数组,用environ变量指向它!示例如下:这个代码就是打印出所有的环境变量!

#include <stdio.h>



int main(void)
{
	extern char **environ;		// 声明就能用
	int i = 0;
	
	while (NULL != environ[i])
	{
		printf("%s\n", environ[i]);
		i++;
	}
	
	
	return 0;
}

虚拟地址空间

我们前面了解过,每个进程的逻辑地址空间为4GB,其中0-1G为OS,1-4G为应用;但是我们进程在运行的时候,只是认为整个系统中的程序只有自己一个,他独自享受着4G内存的荣耀!

所以到这个时候就有虚拟内存的出现了

虚拟内存与物理地址空间的映射的理解:比如我们这个电脑只有512M的一个物理地址存储空间,但是同时需要跑好多个进程,但是单个进程中只认为只有他一个,并且独自享有4G内存,所有就会有虚拟内存与物理地址空间的映射这么一说,比如一个进程他认为自己独占4G,但是他映射到物理地址上只有10M,多个程序都认为自己独占4G,但是他们同时映射到物理地址哦空间上的时候,往往就很小,就感觉像是现实与梦想之间的差距(天上一天,地上一年,最好解释)

进程在链接时可以不用管物理的地址到底是多少了,因为他有0到4g的内存空间,每个进程的链接都是0~4g

第三点

进程正式引入

什么是进程?

进程就是程序的一次运行过程,一个静态的可执行程序a.out的一次运行过程(./a.out去运行到结束)就是一个进程!

进程id

每一个进程都有属于自己的一个名称。他是一个数字,也是在程序过程中我们用于寻找这个进程的一个标识。进程id是由小到大的,不可能重复,就算同一个程序先有一个id,然后进程结束了,这个id被收回,当再次执行这个程序时,他的id号也会重新分配!后面的id 就是紧挨着原先id的!

常见的用于获取进程id的函数有getpid(当前)、getppid(父)、getuid、geteuid、getgid、getegid

对进程调度

(1)操作系统同时运行多个进程
(2)宏观上的并行和微观上的串行
(3)实际上现代操作系统最小的调度单元是线程而不是进程

fork创建子进程

为甚需要创建子进程?

每一次的程序的运行都需要一个进程,多进程可以实现宏观上的并行!

fork的内部原理

创建子进程的过程其实就是分裂生长的模式,如果操作系统需要一个新进程来运行一个程序,那么操作系统会用一个现有的进程来复制生成一个新进程。老进程叫父进程,复制生成的新进程叫子进程。

子进程和父进程的功能是一样的,因为他们是复制产生的,但是子进程和父进程是相互独立的

fork函数调用一次会返回2次,返回值等于0的就是子进程,而返回值大于0的就是父进程(父进程的返回值实质上等于子进程的id)。

典型的使用fork的方法就是:使用fork后然后用if判断返回值,并且返回值大于0时就是父进程,等于0时就是子进程。然后在这个子进程的if语句里去执行一些子进程的命令,在父进程的if语句里去执行一些父进程的命令

如果父进程先释放(死掉)那么他们就将init进程当作这个子进程的父进程。

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

int main(void)
{
	pid_t p1 = -1;
	
	p1 = fork();		// 返回2次
	
	if (p1 == 0)
	{
		// 这里一定是子进程
		
		// 先sleep一下让父进程先运行,先死
		sleep(1);
		
		printf("子进程, pid = %d.\n", getpid());	//打印出来的是子进程的id	
		printf("hello world.\n");
		printf("子进程, 父进程ID = %d.\n", getppid());//打印出来的是父进程的id
	}
	
	if (p1 > 0)
	{
		// 这里一定是父进程
		printf("父进程, pid = %d.\n", getpid());打印出来的是子进程的id
		printf("父进程, p1 = %d.\n", p1);打印出来的是fork的返回值,也就是子进程id
	}
	
	if (p1 < 0)
	{
		// 这里一定是fork出错了
	}
	
	// 在这里所做的操作
	//printf("hello world, pid = %d.\n", getpid());

	return 0;
}

父子进程对文件的操作

父进程先打开一个文件再子进程

父进程先打开一个文件得到fd,然后再fork创建子进程,然后父子进程在各自的进程中去向文件fd中去写入内容。

结果是接续写;此时的父子进程之间其实还有有关联的,子进程对文件的操作,其实是继承了父进程的!

父进程创建子进程后单独操作文件

父进程open打开1.txt然后写入,子进程打开1.txt然后写入,

结果是分别写;父子分别之后才打开文件,这时候两个进程的进程控制块都已经是独立的了,文件也是独立的了,因此两次写文件都是独立的!

open时使用O_APPEND标志看看会如何?实际测试结果标明O_APPEND标志可以把父子进程各自独立打开的fd的文件指针给关联起来,实现分别写。这句话的意思就是把父子进程分别写的内容联系到一块(父进程内容+子进程内容)!

进程的诞生和消亡

进程的诞生- fork/vfork

进程的消亡

一个进程的存在不但有他运行时所要消耗的系统资源(内存、I/O),还有他本身自己占用的内存(8KB,主要是task_struct和栈内存)。

当这个进程消亡时,按照linux系统的设计:操作系统会回收这个进程工作时所消耗的内存和I/O,而并没有回收这个进程本身占用的内存(8KB,主要是task_struct和栈内存)

对于一个进程来说,需要帮助他回收这个进程本身所占内存的的就是他的父进程!!!

僵尸进程

当子进程先于父进程结束,在父进程帮子进程释放子进程本身所占内存前的这段时间,子进程被称之为僵尸进程!!

父进程可以使用wait或waitpid以显式回收子进程的剩余待回收内存资源并且获取子进程退出状态。

父进程也可以不使用wait或者waitpid回收子进程,此时父进程结束时一样会回收子进程的剩余待回收内存资源。

孤儿进程

当父进程先于子进程结束,那么此时子进程被称之为孤儿进程,那当子进程结束时,他的本身所占的内存将会统一被一个特殊的进程(进程1,也就是init进程)所释放;

父进程回收子进程

wait的工作原理

当子进程结束后,系统会向父进程发送一个SIGCHILD的一个信号(这个信号主要是解决父子进程之间异步通信的问题),当父进程接收到这个信号时,他就会调用wait函数去回收子进程,wait函数是一个阻塞函数,当调用了wait后,如果子进程还没有死,那么父进程就会一直阻塞在wait那,等待信号去唤醒它。

如果父进程没有任何子进程,然后调用wait会报错!

wait函数的形参是一个输出型参数,他会返回一些子进程结束时的状态,父进程通过wait得到status后就可以知道子进程的一些结束状态信息。

那这些状态信息如何看呢?系统引入了一些宏实现这个功能!

WIFEXITED宏用来判断子进程是否正常终止(return、exit、_exit退出)
WIFSIGNALED宏用来判断子进程是否非正常终止(被信号所终止)
WEXITSTATUS宏用来得到正常终止情况下的进程返回值的。

 waitpid介绍(和wait一样 都是回收子进程 可以回收指定PID的子进程)

上面是 waitpid这个函数的一些形参返回值

pid_t pid表示某个特定的子进程的pid ,如果这个值为-1,就代表不等待某个特定的pid的子进程而是回收任意一个子进程。

int options为0表示用默认的方式(阻塞式)来进行等待;如果为WNOHANG表示回收方式为非阻塞式;

返回值pid_t表示本次回收的子进程pid.

在阻塞状态下:如果当前进程并没有一个ID号为pid的子进程,则返回值为-1;如果成功回收了pid这个子进程则返回值为回收的进程的PID

在非阻塞状态下:如果成功回收了pid这个子进程则返回值为回收的进程的PID,如果父进程waitpid时子进程尚未结束则父进程立刻返回(非阻塞),但是返回值为0(表示回收不成功)。

exec族函数

为何需要exec函数

父进程fork创建了子进程后,要使父子进程各自执行单个的程序,有一种方法就是在子进程的if语句中去写子进程执行的源代码;但是;首先必须知道源代码,而且源代码太长了也不好控制!

因此,exec函数应运而生,我们可以再编写一个子程序执行的代码,然后形成可执行程序,然后在子进程的if语句中去用exec函数调用这个可执行程序!

exec族函数可以直接把一个编译好的可执行程序直接加载运行

exec族的6个函数

execl和execv 这两个函数是最基本的exec,都可以用来执行一个程序,区别是传参的格式不同。

execl的传参,第一个是可执行程序的全路径,后面是参数列表,最后一个是NULL.

execv的传参,第一个是可执行程序的全路径,后面是是把参数列表事先放入一个字符串数组中,再把这个字符串数组传给execv函数。

execlp和execvp 这两个函数在上面2个基础上加了p,较上面2个来说,区别是:上面2个执行程序时必须指定可执行程序的全路径(如果exec没有找到path这个文件则直接报错),而加了p的传递的可以是file(也可以是path,只不过兼容了file。加了p的这两个函数会首先去找file,如果找到则执行执行,如果没找到则会去环境变量PATH所指定的目录下去找,如果找到则执行如果没找到则报错)

进程状态与system函数

进程有五个状态

(1)就绪态。这个进程当前所有运行条件就绪,只要得到了CPU时间就能直接运行。
(2)运行态。就绪态时得到了CPU就进入运行态开始运行。
(3)僵尸态。进程已经结束但是父进程还没来得及回收
(4)等待态(浅度睡眠&深度睡眠),进程在等待某种条件,条件成熟后可进入就绪态。等待态下就算你给他CPU调度进程也无法执行。浅度睡眠等待时进程可以被(信号)唤醒,而深度睡眠等待时不能被唤醒只能等待的条件到了才能结束睡眠状态。
(5)暂停态。暂停并不是进程的终止,只是被被人(信号)暂停了,还可以恢复的。

在这里插入图片描述

system函数简介

system函数相当于fork+exec!!

system是原子操作,但fork+exec是非原子操作!!

原子操作意思就是整个操作一旦开始就会不被打断的执行完。原子操作的好处就是不会被人打断(不会引来竞争状态),坏处是自己单独连续占用CPU时间太长影响系统整体实时性,因此应该尽量避免不必要的原子操作,就算不得不原子操作也应该尽量原子操作的时间缩短。

进程关系

进程组(group)由若干进程构成一个进程组 - 会话(session)会话就是进程组的组

守护进程

守护进程的引入(what)

进程查看命令ps

ps -ajx 偏向显示各种有关的ID号

ps -aux 偏向显示进程各种占用资源

什么是守护进程?

守护进程(daemon)他是与控制台脱离,长期运行,一般是是从开机开始运行到关机的一种进程,我们常见的守护进程的举例;比如说服务器:就是一个一直在运行的程序,可以给我们提供某种服务,当我们程序需要这种服务时,就可以调用服务器程序来进行这种服务操作。

常见的守护进程:syslogd,系统日志守护进程,提供syslog功能、cron,cron进程用来实现操作系统的时间管理,linux中实现定时执行程序的功能就要用到cron

向进程发送信号指令kill

(1)kill -信号编号 进程ID,向一个进程发送一个信号
(2)kill -9 xxx,将向xxx这个进程发送9号信号,也就是要结束进程

编写简单的守护进程

任何一个进程都可以将自己实现成守护进程;守护进程的创建需要以下步骤!

子进程等待父进程退出        //孤儿进程
子进程使用setsid创建新的会话期,脱离控制台    //摆脱终端
调用chdir将当前工作目录设置为 /       //保护数据
umask设置为0以取消任何文件权限屏蔽   //方便操作文件
关闭所有文件描述符    //释放不必要的资源 
将0、1、2定位到/dev/null      

第四点

进程通信概括

进程中为啥需要通信

进程间通信(IPC)指的是2个任意进程之间的通信

同一个进程在一个地址空间中,所以同一个进程的不同模块(不同函数、不同文件)之间都是很简单的(很多时候都是全局变量、也可以通过函数形参实参传递)

2个不同的进程处于不同的地址空间,因此要互相通信很难

IPC技术在一般中小型程序中用不到,在大型程序中才会用到。

linux内核提供多种进程间通信机制

无名管道和有名管道

管道(无名管道)
管道通信的原理:内核维护的一块内存,有读端和写端(管道是单向通信的
管道通信的方法:父进程创建管理后fork子进程,子进程继承父进程的管道fd
管道通信的限制:只能在父子进程间通信、半双工
管道通信的函数:pipe、write、read、close

在这里插入图片描述

通信原理:比如8是父进程,然后在8中利用pipe的API创建一个无名管道,因此获得两个文件描述符,一个是读,一个是写,然后在8进程中再fork一个子进程,比如说是进程11,那么进程11自然而然的继承了进程8的管道以及内存资源,并且管道是用于父子间的半双工通信,那么就只能单向通信,如果需要8写11读,那就可以把8的读和11写的文件描述符进行作废处理,已达成半双工通信的标准!​​​​​​​
有名管道(fifo-first in first out)
有名管道的原理:实质也是内核维护的一块内存,表现形式为一个有名字的文件
有名管道的使用方法:固定一个文件名,2个进程分别使用mkfifo创建fifo文件,然后分别open打开获取到fd,然后一个读一个写
管道通信限制:半双工(注意不限父子进程,任意2个进程都可
管道通信的函数:mkfifo、open、write、read、close在这里插入图片描述​​​​​​​

SystemV IPC介绍

(1)系统通过一些专用API来提供SystemV IPC功能
(2)分为:信号量、消息队列、共享内存​​​​​
(3)其实质也是内核提供的公共内存

消息队列(适合做广播)

消息队列本质上就是一个队列,队列本质上可以理解为(内核维护的一个)FIFO

比如:现在需要A进程和B进程进行通信,那么A进程向队列中放入消息,B从队列中读出消息。现实中举个例子;就是天气预报播放室,比如大屏幕和服务器时两个进程,服务器负责写,大屏幕负责读,那么这就是消息队列通信。滚动播放,符合FIFO的特点!

信号量(提供互斥和同步)

实质上就是个计数器(其实就是一个可以用来技术的变量,可以理解为int a),通过信号量来提供互斥和同步!

配合互斥|同步访问比如说是停车场,本来是100个车位,现在已经有99个车位占用了(假设这99车固定不动),现在有A和B两个车想停车,但在同一时间内,只能停一个车啊,那这就是互斥,如果A车率先开进去了,那么显示牌就会显示0(剩余量),此时B车就只能等待,等待A车出来,那么这时候显示牌就会显示1(剩余量),那么B车才能停,这就是信号量配合互斥访问!

以前我们讨论同步的时候是考虑在同一时钟下,进程A和进程B通过时钟信号达成某一种动作,但是在我们现在讨论的进程A和进程B,其实是一种异步,没有时钟信号进行同步,那这里通过信号量就可以实现同步,还是看上面这个例子,车A和车B就是通过观察信号量的值进行动作的变化(A车进,B车就等待,反之则反),按一定的顺序去做某件事,这就是同步的定义!

共享内存(适合大量数据的交换)

大片的内存直接映射,这就是与之前消息队列、和管道之间的简单区别吧,对于消息队列和管道来说,他们只是负责少量数据的通信,但是共享内存负责的是大量数据的一个交互,类似于LCD显示时的显存用法。具体的用法在智能音箱项目中有使用!

进程间通信——共享内存(Shared Memory)_YPT_victory的博客-CSDN博客_共享内存

Socket域套接字

属于网络编程,用于不同计算机之间的进程间通信!(后面再网络变成部分再详细探讨)


信号(下一篇详细讨论)

用于通知接收进程某个事件已经发生,是一种比较复杂的通信方式!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值