Linux-应用编程-学习笔记(17):进程全解

Linux-应用编程-学习笔记(17):进程全解

前言:当程序被系统调用到内存以后,系统会给程序分配一定的资源(内存,设备等等)然后进行一系列的复杂操作,使程序变成进程以供系统调用,因此进程是linux系统中非常重要的一个概念。

一、程序和环境变量

1. 程序的开始和结束

1.1 运行前的准备
之前一直在说一个程序的开始是main函数,但是在main函数执行之前,操作系统下的应用程序还是需要执行一段引导代码才能去执行main函数。然后这段引导代码一般不需要我们自己去编写,而是在编译连接时由链接器编译器中事先准备好的引导代码给连接进去和我们的应用程序一起构成最终的可执行程序。

1.2 加载时
加载器是操作系统中的程序,当我们去执行一个程序时(譬如./a.out,譬如代码中用exec族函数来运行)加载器负责将这个程序加载到内存中去执行这个程序。
因此一个程序在编译连接时用链接器,运行时用加载器

1.3 程序的退出
(1)正常终止:return、exit、_exit(return -1和return 0都是自己知道为什么终止)。
(2)非正常终止:自己或他人发信号(Ctrl C就是一个信号)。
(3)还可以通过atexit函数来注册进程终止处理函数(也就是退出前执行某一个程序)。当用atexit注册多个进程终止处理函数,先注册的后执行(先进后出,和栈一样)。

atexit函数用法如下:

#include 
#include 
#include 

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

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

int main(void)
{
	printf("hello world.\n");
	
	// 当进程被正常终止时,系统会自动调用这里注册的func1执行
	atexit(func2);
	atexit(func1);
	
	printf("my name is lilei hanmeimei\n");	
	//return 0;
	exit(0);
}

2. 进程环境

(1)我们可以在ubuntu中执行export命令来查看进程环境变量。
Linux-应用编程-学习笔记(17):进程全解_第1张图片
(2)每一个进程中都有一份所有环境变量构成的一个表格,也就是说我们当前进程中可以直接使用这些环境变量。进程环境表其实是一个字符串数组,用environ变量指向它。

//包含库文件
#include 
//用指针指向变量
extern char **environ;
//然后直接使用它
printf("%s\n", environ[i]);

我们写的程序中可以无条件直接使用系统中的环境变量,所以一旦程序中用到了环境变量那么程序就和操作系统环境有关了。

二、进程解析

1. 进程的引入

1.1 什么是进程?
首先,进程是一个动态过程而不是静态实物。其次,进程就是程序的一次运行过程,一个静态的可执行程序a.out的一次运行过程(./a.out去运行到结束)就是一个进程。内核中构建了一种数据结构,又称进程控制块PCB(process control block),用来专门管理一个进程。

1.2 进程运行的虚拟地址空间
(1)操作系统中每个进程在独立地址空间中运行,所以进程直接的运行不会相互打扰。这提供了一种进程隔离的方法,从而能够提供一种多进程同时运行的方式。
(2)如果进程运行所在的系统为4GB内存时,那么每个进程的逻辑地址空间均为4GB(32位系统)。但是在实际的情况下,一个进程所占用的内存基本不会达到4GB这么多,也就是不会用满。因此,内存的使用一般会被划分为2部分,0-1G为OS,1-4G为应用,对于每个进程来说,它们自己都会认为它们在独享着这4GB的内存,但是实际情况是这4GB的内存被合理地供多个进程一起使用(这样也是为了能够提高系统效率)。
(3)那么怎样做到了每个进程能够独享4GB空间?这就用到了虚拟地址到物理地址空间的映射的方法,也就是进程用到多少内存,就在物理地址上映射多少。所以内存进行分时复用的方式,实现让每个进程觉得自己都用到了大内存。

1.3 进程的ID
操作系统给每一个进程一个ID号来标识这个进程。我们可以通过ps命令在ubuntu中查看当前运行的进程信息,其中PID即为进程的ID号。
Linux-应用编程-学习笔记(17):进程全解_第2张图片
同时,也可以通过系统API来实现函数获取进程ID的方法。
getpid(获取当前进程ID)、getppid(获取父进程ID)、getuid(获取当前进程的用户ID)、geteuid(有效用户ID)、getgid(获取组ID)、getegid(有效组ID)

//包含的头文件
#include 
#include 
//定义
pid_t p1 = -1, p2 = -1;
//使用
p1 = getpid();
printf("pid = %d.\n", p1);	

1.4 多进程调度原理
操作系统是一种多进程的方式,因此在操作系统中,可以同时运行多个进程。宏观上多进程是一种并行的方式,但是在微观上CPU需要在多个进程之间实现切换,对于CPU是一种串行的方式。如何实现在多个进程之间的切换?这就用到了调度器
操作系统的调度器其实就是一个算法,它用来决定先运行谁,后运行谁,谁运行多长时间。(因为CPU的速度非常快,通过多进程与调度系统来提高CPU的利用率)。实际上现代操作系统最小的调度单元是线程而不是进程。

2. 父子进程关系

2.1 子进程的创建
(1)每一次程序的运行都需要一个进程,因此诞生一个新的进程即标准着一个子进程的创建。
(2)由于操作系统中构建一个全新的进程是一件非常复杂的事情,所以为了简化这件事情,构建新的进程采用分裂生长模式。如果操作系统需要一个新进程来运行一个程序,那么操作系统会用一个现有的进程来复制生成一个新进程。老进程叫父进程,复制生成的新进程叫子进程。
(3)创建完成之后的父子进程会同时运行。
Linux-应用编程-学习笔记(17):进程全解_第3张图片
我们可以通过系统API中的fork函数来实现子进程的创建,原理就是上面提到的进程复制。

//头文件包含
#include 
//代码用法
pid_t fork(void);
//使用举例
#include 
#include 
#include 

int main(void)
{
	pid_t p1 = -1;
	//创建一个子进程
	p1 = fork();		// 返回2次
	//fork函数调用一次会返回2次,返回值等于0的就是子进程,而返回值大于0的就是父进程
	if (p1 == 0)
	{
		// 这里一定是子进程
		// 先sleep一下让父进程先运行,先死
		sleep(1);		
		printf("子进程, pid = %d.\n", getpid());		
		printf("hello world.\n");
		printf("子进程, 父进程ID = %d.\n", getppid());
	}
	if (p1 > 0)
	{
		// 这里一定是父进程
		printf("父进程, pid = %d.\n", getpid());
		printf("父进程, p1 = %d.\n", p1);
	}	
	if (p1 < 0)
	{
		// 这里一定是fork出错了
	}
	return 0;
}

Linux-应用编程-学习笔记(17):进程全解_第4张图片
从最后的运行结果可以看出,main函数相当于被运行了2次,这证明了父子进程是同时运行的。通过函数中,我们可以知道,fork函数调用一次会返回2次,返回值等于0的就是子进程,而返回值大于0的就是父进程,因此我们可以通过if判断返回值来指定父进程和子进程的任务。

对于新生成的子进程来说,它继承了一些父进程的属性,但是它有着自己独立的PCB,同时它也能被内核进行同等调度。

2.2 父子进程对文件的操作
(1)父进程先open打开一个文件得到fd,然后在fork创建子进程。之后在父子进程中各自write向fd中写入内容。
测试结论:接续写。实际上本质原因是父子进程之间的fd对应的文件指针是彼此关联的(很像O_APPEND标志后的样子)。
实际测试时有时候会看到只有一个,有点像分别写。但是实际不是,原因是父进程执行完就close了,子进程就写不进去了(解决方法是给每个进程中添加sleep,这样就不会存在一个关闭了,另一个没法写)。
(2)父进程open打开1.txt然后写入,子进程打开1.txt然后写入,也就是各自独立打开同一文件。
测试结论:分开写。原因是父子进程分离后才各自打开的1.txt,这时候这两个进程的PCB已经独立了,文件表也独立了,因此2次读写是完全独立的。
open时使用O_APPEND标志看看会如何?实际测试结果标明O_APPEND标志可以把父子进程各自独立打开的fd的文件指针给关联起来,实现分别写。

总结:
父子进程间终究多了一些牵绊(存在一些继承的东西)。父进程在没有fork之前自己做的事情对子进程有很大影响,但是父进程fork之后在自己的if里做的事情就对子进程没有影响了。本质原因就是因为fork内部实际上已经复制父进程的PCB生成了一个新的子进程,并且fork返回时子进程已经完全和父进程脱离并且独立被OS调度执行。总之就是一句话,子进程最终目的是要独立去运行另外的程序

3. 进程的诞生和消亡

3.1 进程的诞生
我们知道在最开始的时候先有的是进程0和进程1,进程0为内核态,进程1为内核态过度到用户态的一个重要进程,也可以说所有的进程都源于进程1。进程产生的原理就是上述提到的fork形式创建。

3.2 进程的消亡
进程终止有两种方式,一种是自己设定的终止方式,又称正常终止;另一种是由于一些信号或者外部缘故导致的终止,成为异常终止。
进程在运行时需要消耗系统资源(内存、IO)。linux系统设计时规定:每一个进程退出时,操作系统会自动回收这个进程涉及到的所有的资源(譬如malloc申请的内容没有free时,当前进程结束时这个内存会被释放,譬如open打开的文件没有close的在程序终止时也会被关闭)。但是操作系统只是回收了这个进程工作时消耗的内存和IO,而并没有回收这个进程本身占用的内存(8KB,主要是task_struct和栈内存)。那么这8KB的内存操作系统该怎么办?答案是需要通过他的父进程来帮它处理这些残余的资源

3.3 僵尸进程和孤儿进程
(1)僵尸进程
子进程先于父进程结束。子进程结束后父进程此时并不一定立即就能帮子进程“收尸”,在这一段(子进程已经结束且父进程尚未帮其收尸)子进程就被成为僵尸进程。
在这种情况下,子进程结束时,操作系统会将其占用的大量资源进行回收。但是8KB的参与需要通过父进程使用wait或waitpid显式回收子进程的剩余待回收内存资源并且获取子进程退出状态
父进程结束时一样会回收子进程的剩余待回收内存资源。(这样设计是为了防止父进程忘记显式调用wait/waitpid来回收子进程从而造成内存泄漏)
(2)孤儿进程
父进程先于子进程结束,子进程成为一个孤儿进程。
linux系统规定:所有的孤儿进程都自动成为一个特殊进程(进程1,也就是init进程)的子进程。或者对于ubuntu这种操作系统会提供一个统一的进程进行孤儿进程的管理。

3.4 父进程对子进程回收
(1)wait回收方式
工作原理:子进程结束时,系统会向其父进程发送SIGCHILD信号。因此,父进程调用wait函数后阻塞,等待被SIGCHILD信号唤醒然后去回收僵尸子进程。(如果父进程没有任何子进程则wait返回错误)
父子进程之间是异步的,SIGCHILD信号机制就是为了解决父子进程之间的异步通信问题,让父进程可以及时的去回收僵尸子进程。

//包含头文件
#include 
#include 
//函数用法
pid_t wait(int *status);
//函数举例
#include 
#include 
#include   
#include 
#include 

int main(void)
{
	pid_t pid = -1;
	pid_t ret = -1;
	int status = -1;
	//创建一个子进程	
	pid = fork();
	if (pid > 0)
	{
		// 父进程
		sleep(1);
		printf("parent.\n");
		ret = wait(&status);
		
		printf("子进程已经被回收,子进程pid = %d.\n", ret);
		printf("子进程是否正常退出:%d\n", WIFEXITED(status));
		printf("子进程是否非正常退出:%d\n", WIFSIGNALED(status));
		printf("正常终止的终止值是:%d.\n", WEXITSTATUS(status));
	}
	else if (pid == 0)
	{
		// 子进程
		printf("child pid = %d.\n", getpid());
		return 51;
		//exit(0);
	}
	else
	{
		perror("fork");
		return -1;
	}
	
	return 0;
}

Linux-应用编程-学习笔记(17):进程全解_第5张图片
wait的参数status。status用来返回子进程结束时的状态,父进程通过wait得到status后就可以知道子进程的一些结束状态信息。wait的返回值pid_t,这个返回值就是本次wait回收的子进程的PID。当前进程有可能有多个子进程,wait函数阻塞直到其中一个子进程结束wait就会返回,wait的返回值就可以用来判断到底是哪一个子进程本次被回收了。
对wait做个总结:wait主要是用来回收子进程资源,回收同时还可以得知被回收子进程的pid和退出状态

(2)waitpid回收方式
工作原理:与wait原理相同,唯一区别是waitpid支持非阻塞的方式(也就是我waitpid看一下有没有信号表示进程需要被回收,如果没有就直接过去了)。

//包含头文件
#include 
#include 
//函数用法
pid_t waitpid(pid_t pid, int *status, int options);
//函数举例
#include 
#include 
#include   
#include 
#include 

int main(void)
{
	pid_t pid = -1;
	pid_t ret = -1;
	int status = -1;
	
	pid = fork();
	if (pid > 0)
	{
		// 父进程
		sleep(1);	//这里相当于一种消灭竟态的方法
		printf("parent, 子进程id = %d.\n", pid);
		//ret = wait(&status);
		//ret = waitpid(-1, &status, 0);
		//ret = waitpid(pid, &status, 0);
		ret = waitpid(pid, &status, WNOHANG);		// 非阻塞式
		
		printf("子进程已经被回收,子进程pid = %d.\n", ret);
		printf("子进程是否正常退出:%d\n", WIFEXITED(status));
		printf("子进程是否非正常退出:%d\n", WIFSIGNALED(status));
		printf("正常终止的终止值是:%d.\n", WEXITSTATUS(status));
	}
	else if (pid == 0)
	{
		// 子进程
		//sleep(1);
		printf("child pid = %d.\n", getpid());
		return 51;
		//exit(0);
	}
	else
	{
		perror("fork");
		return -1;
	}
	
	return 0;
}

(1)ret = waitpid(-1, &status, 0); -1表示不等待某个特定PID的子进程而是回收任意一个子进程,0表示用默认的方式(阻塞式)来进行等待,返回值ret是本次回收的子进程的PID。
(2)ret = waitpid(pid, &status, 0); 等待回收PID为pid的这个子进程,如果当前进程并没有一个ID号为pid的子进程,则返回值为-1;如果成功回收了pid这个子进程则返回值为回收的进程的PID。
(3)ret = waitpid(pid, &status, WNOHANG);这种表示父进程要非阻塞式的回收子进程。此时如果父进程执行waitpid时子进程已经先结束等待回收则waitpid直接回收成功,返回值是回收的子进程的PID;如果父进程waitpid时子进程尚未结束则父进程立刻返回(非阻塞),但是返回值为0(表示回收不成功)。

4. exec族函数

4.1 为什么需要exec函数?
fork子进程是为了执行新程序(fork创建了子进程后,子进程和父进程同时被OS调度执行,因此子进程可以单独的执行一个程序,这个程序宏观上将会和父进程程序同时进行)。
可以直接在子进程的if中写入新程序的代码。这样可以,但是不够灵活,因为我们只能把子进程程序的源代码贴过来执行(必须知道源代码,而且源代码太长了也不好控制),譬如说我们希望子进程来执行ls -la 命令就不行了(没有源代码,只有编译好的可执行程序)。使用exec族运行新的可执行程序exec族函数可以直接把一个编译好的可执行程序直接加载运行)。
我们有了exec族函数后,我们典型的父子进程程序是这样的:子进程需要运行的程序被单独编写、单独编译连接成一个可执行程序(叫hello),(项目是一个多进程项目)主程序为父进程,fork创建了子进程后在子进程中exec来执行hello,达到父子进程分别做不同程序同时(宏观上)运行的效果

4.2 exec族的6个参数
(1)execl和execv:这两个函数是最基本的exec,都可以用来执行一个程序,区别是传参的格式不同。execl是把参数列表(本质上是多个字符串,必须以NULL结尾)依次排列而成(l其实就是list的缩写),execv是把参数列表事先放入一个字符串数组中,再把这个字符串数组传给execv函数。
(2)execlp和execvp:这两个函数在上面2个基础上加了p,较上面2个来说,区别是:上面2个执行程序时必须指定可执行程序的全路径(如果exec没有找到path这个文件则直接报错),而加了p的传递的可以是file(也可以是path,只不过兼容了file。加了p的这两个函数会首先去找file,如果找到则直接执行,如果没找到则会去环境变量PATH所指定的目录下去找,如果找到则执行如果没找到则报错)
(3)execle和execvpe:这两个函数较基本exec来说加了e,函数的参数列表中也多了一个字符串数组envp形参,e就是environment环境变量的意思,和基本版本的exec的区别就是:执行可执行程序时会多传一个环境变量的字符串数组给待执行的程序。

//头文件包含
#include 
//函数用法
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 execvpe(const char *file, char *const argv[],
           char *const envp[]);
//函数举例
#include 
#include 
#include   
#include 
#include 

int main(void)
{
	pid_t pid = -1;
	pid_t ret = -1;
	int status = -1;
	
	pid = fork();
	if (pid > 0)
	{
		// 父进程
		printf("parent, 子进程id = %d.\n", pid);
	}
	else if (pid == 0)
	{
		// 子进程
		//execl("/bin/ls", "ls", "-l", "-a", NULL);		// ls -l -a
		//char * const arg[] = {"ls", "-l", "-a", NULL};
		//execv("/bin/ls", arg);
		
		//execl("hello", "aaa", "bbb", NULL);
		//char * const arg[] = {"aaa", "bbb", NULL};
		//execv("hello", arg);
		
		//execlp("ls", "ls", "-l", "-a", NULL);	
		char * const envp[] = {"AA=aaaa", "XX=abcd", NULL};
		execle("hello", "hello", "-l", "-a", NULL, envp);
		
		return 0;
	}
	else
	{
		perror("fork");
		return -1;
	}
	
	return 0;
}

5. 进程状态和关系

5.1 进程的5种状态
(1)就绪态。当前所有运行条件均就绪,只要得到了CPU时间就能直接运行。
(2)运行态。就绪态时得到了CPU就进入运行态开始运行。
(3)僵尸态。进程已经结束但是父进程还没来得及回收。
(4)等待态(浅度睡眠&深度睡眠),进程在等待某种条件,条件成熟后可进入就绪态。等待态下就算你给他CPU调度进程也无法执行。浅度睡眠等待时进程可以被(信号)唤醒,而深度睡眠等待时不能被唤醒只能等待的条件到了才能结束睡眠状态。
(5)暂停态。暂停并不是进程的终止,只是被被人(信号)暂停了,还可以恢复的。
Linux-应用编程-学习笔记(17):进程全解_第6张图片
Linux-应用编程-学习笔记(17):进程全解_第7张图片
5.2 进程间的关系
(1)无关系:两个进程之间是完全独立的。无关的进程之间不能随便访问。
(2)父子进程:父进程继承给子进程的东西都是fork之前的。子进程变为僵尸进程后,需要父进程对它进行回收。
(3)进程组(group):由若干进程构成一个进程组。也就是属主所在的组,他们的进程组ID相同,放在一个组的目的是为了方便管理这些进程。组内进程的关系比组外的更有联系。
(4)会话(session):会话就是进程组的组。由若干进程组构成的组,就像几个班构成一个年级一样。

6. 守护进程

6.1 什么是守护进程?
daemon,表示守护进程,简称为d。通过ps指令查看的进程,有一些名字后面带d的(基本上就是守护进程)。守护进程是一种长期运行的状态(一般是开机运行直到关机时关闭)。它与控制台脱离(普通进程都和运行该进程的控制台相绑定,表现为如果终端被强制关闭了则这个终端中运行的所有进程都会被关闭,背后的问题还在于会话,一个终端运行的所有进程同属于一个会话,所以终端关闭之后,也就表示会话关闭,所以里面的进程会全部被关闭。然而对于守护进程来说,只能通过kill -9 xxx来实现进程的退出)。
Linux-应用编程-学习笔记(17):进程全解_第8张图片
注:ps查看当前目录下的进程,ps -ajx偏向显示各种有关的ID号,ps -aux偏向显示进程各种占用资源。

从图中我们可以看到,对于守护进程,它的TTY为?,表示不依赖于某个终端。

6.2 守护进程的作用
守护进程就是一个后台一直运行的程序,它可以后台帮助我们完成一些任务。例如服务器(Server),服务器程序就是一个一直在运行的程序,可以给我们提供某种服务(譬如nfs服务器给我们提供nfs通信方式),当我们程序需要这种服务时我们可以调用服务器程序(和服务器程序通信以得到服务器程序的帮助)来进行这种服务操作。服务器程序一般都实现为守护进程。守护进程的打开为打开某个程序时,该程序内部有打开该守护进程的代码,即使该程序结束了,它内部的守护进程也不会随之结束
常见的守护进程还有:syslogd,系统日志守护进程,提供syslog功能。cron,用来实现操作系统的时间管理,linux中实现定时执行程序的功能就要用到cron。

6.3 实现简单的守护进程
任何一个进程都可以将自己实现成守护进程,实现守护进程需要具有如下几个要素:
(1)子进程等待父进程退出(变为孤儿进程)
(2)子进程使用setsid创建新的会话期,脱离控制台
(3)调用chdir将当前工作目录设置为/
(4)umask设置为0以取消任何文件权限屏蔽
(5)关闭所有文件描述符
(6)将0、1、2定位到/dev/null(这个相当于回收站的存在)

#include 
#include 
#include 
#include 
#include 
#include 

void create_daemon(void);

int main(void)
{
	create_daemon();
	while (1)
	{
		printf("I am running.\n");	
		sleep(1);
	}	
	return 0;
}

// 函数作用就是把调用该函数的进程变成一个守护进程
void create_daemon(void)
{
	pid_t pid = 0;
	//创建一个子进程
	pid = fork();
	if (pid < 0)
	{
		perror("fork");
		exit(-1);
	}
	if (pid > 0)
	{
		exit(0);		// (1)父进程直接退出
	}
	
	// 执行到这里就是子进程
	//(2)setsid将当前进程设置为一个新的会话期session,目的就是让当前进程脱离控制台。
	pid = setsid();
	if (pid < 0)
	{
		perror("setsid");
		exit(-1);
	}
	
	//(3)将当前进程工作目录设置为根目录
	chdir("/");	
	//(4)umask设置为0确保将来进程有最大的文件操作权限
	umask(0);
	
	//(5)关闭所有文件描述符
	// 先要获取当前系统中所允许打开的最大文件描述符数目
	int cnt = sysconf(_SC_OPEN_MAX);
	int i = 0;
	for (i=0; i<cnt; i++)
	{
		close(i);
	}
	//(6)将0、1、2定位到/dev/null
	open("/dev/null", O_RDWR);
	open("/dev/null", O_RDWR);
	open("/dev/null", O_RDWR);
}

由于守护进程无法通过正常的关闭控制台来退出,每执行一次就会出现一个进程,那么有什么方法来保证程序不被多次运行呢?
答:我们希望我们的程序具有一个单例运行的功能。意思就是说当我们./a.out去运行程序时,如果当前还没有这个程序的进程运行则运行之,如果之前已经有一个这个程序的进程在运行则本次运行直接退出(提示程序已经在运行)。最常用的一种方法就是:用一个文件的存在与否来做标志。具体做法是程序在执行之初去判断一个特定的文件是否存在,若存在则标明进程已经在运行,若不存在则标明进程没有在运行。然后运行程序时去创建这个文件。当程序结束的时候去删除这个文件即可。

7. 进程间通信

7.1 为什么要进行进程间通信?
(1)进程间通信(IPC)指的是2个任意进程之间的通信。(通信说白了就是在我这里变了,你那里能够知道我的改变)
(2)同一个进程在一个地址空间中,所以同一个进程的不同模块(不同函数、不同文件)之间都是很简单的(很多时候都是全局变量、也可以通过函数形参实参传递)
(3)2个不同的进程处于不同的地址空间,因此要互相通信很难。

7.2 什么样的程序设计需要进程间通信?
(1)99%的程序是不需要考虑进程间通信的。因为大部分程序都是单进程的(可以多线程)
(2)复杂、大型的程序,因为设计的需要就必须被设计成多进程程序(我们整个程序就设计成多个进程同时工作来完成的模式),常见的如GUI、服务器。
(3)结论:IPC技术在一般中小型程序中用不到,在大型程序中才会用到。

7.3 进程间通信方式
1、管道
管道分为无名管道有名管道,默认情况下说管道的话都指的是无名管道。无名管道指抽象出来的文件是没有名字的,有名管道表现形式为一个有名字的文件(相当于一个暗号)。
Linux-应用编程-学习笔记(17):进程全解_第9张图片
(1)无名管道:
管道通信的原理:内核维护的一块内存,有读端和写端(管道是单向通信的),通过一种读写文件的方式来实现进程御锦城之间的通信(A进程将信息写入到内存文件中,B进程去内存文件中读取内容)。
管道通信的限制:由于管道通信是一种类似于读写文件的形式。所以只能在父子进程间通信(因为是通过继承fd来完成的)。并且有可能被别人抢读,或者自己写的自己读了。
管道通信的函数:pipe、write、read、close
为了解决管道通信的稳定性,我们采用了将每个进程阉割一部分读写功能的方式(例如上图中将进程200的写功能和进程201的读功能去除,实现一个单工管道),同样的方法进行第二条管道的设计,最终实现一种2个单工通道构成的半双工通道,实现双管道的通信方式

(2)有名管道:
管道通信的原理:与无名管道的原理相同,只不过是读写的文件是一个有名字的文件。
有名管道的使用方法:固定一个文件名,2个进程分别使用mkfifo创建fifo文件,然后分别open打开获取到fd,然后一个读一个写。
管道通信限制:半双工(注意不限父子进程,任意2个进程都可,这是因为读写的文件是一个实际存在的有名文件)。
管道通信的函数:mkfifo、open、write、read、close。

2、SystemV IPC
系统通过一些专用API来提供SystemV IPC功能,它分为信号量、消息队列、共享内存。它的实质也是内核提供的公共内存。

(1)消息队列:
本质上是一个队列,队列可以理解为(内核维护的一个)FIFO。入队列就相当于往队尾放,出队列就相当于从队列头取。
工作时A和B2个进程进行通信,A向队列中放入消息,B从队列中读出消息

(2)信号量:
实质就是个计数器(其实就是一个可以用来计数的变量,可以理解为int a)。
通过计数值来提供互斥(某个进程在使用某个东西时,先去检查信号量为0还是1,如果为0表示空闲,则该进程可以去用这个东西,但是在用之前要先将信号量变为1,这样别的进程想使用这个东西时检查信号量为1,它就不可以使用。并且在用完时要将信号量重新变为0)和同步(有一个公共的信号量为S,A进程走一步后给S加1,B进程去检查这个S来调整自己的步伐,我用完了给你你用完了给我,相当于一个锁)。互斥和同步便是信号量所完成的两个进程之间的2种常用的通信需求

(3)共享内存:
大片内存直接映射(两个进程共享一块内存,一个进程写入指定的地方,然后另一个进程去指定地方读,最终实现了一种好像复制过去的感觉)。类似于LCD显示时的显存用法。

3、信号:见第三章信号。

4、Socket域套接字:是一种网络编程的形式。

三、linux中的信号

1. 什么是信号?

信号是内容受限的一种异步通信机制,它的目的是用来实现通信。信号可以被理解成一种软件中断,它本质上是int型的数字编号(大部分是实现设定好的)。

信号由谁发出?
(1)用户在终端按下按键(比如ctrl+C)
(2)硬件异常后由操作系统内核发出信号(比如除以0以后,计算机检查到会自动报错)
(3)用户使用kill命令向其他进程发出信号
(4)某种软件条件满足后也会发出信号,如alarm闹钟时间到会产生SIGALARM信号,向一个读端已经关闭的管道write时会产生SIGPIPE信号

信号有什么处理方法?
(1)忽略信号(相当于别人发送了信息,我不回)
(2)捕获信号(信号绑定了一个函数,通过这个信号去做了有意义的事情)
(3)默认处理(当前进程没有明显的管这个信号,默认:忽略或终止进程),操作系统给每个信号定义了一个默认的动作,如果不是显示的操作这个信号,那么则会对应系统制定的默认方式。主动的处理方式会去覆盖默认处理方式

2. 常见的信号介绍

信号名字 信号编号 处理内容
SIGINT 2 Ctrl+C时OS送给前台进程组中每个进程(int为interrupt,有打断中断的意思)
SIGABRT 6 调用abort函数,进程异常终止
SIGPOLL/SIGIO 8 指示一个异步IO事件,在高级IO中提及
SIGKILL 9 杀死进程的终极办法(该信号不能被忽略)
SIGSEGV 11 无效存储访问时OS发出该信号(访问了不该访问的位置)
SIGPIPE 13 涉及管道和socket
SIGALARM 14 涉及alarm函数的实现
SIGTERM 15 kill命令发送的OS默认终止信号
SIGCHLD 17 子进程终止或停止时OS向其父进程发此信号(回收僵尸进程时候使用)
SIGALARM 14 涉及alarm函数的实现
SIGUSR1 10 用户自定义信号,作用和意义由应用自己定义
SIGUSR2 12 用户自定义信号,作用和意义由应用自己定义

信号是一开始就定义好了名字,编号和作用内容的,因此调用对应的信号就可以实现对应的效果。SIGUSR1和SIGUSR2这两个信号是需要用户去指定作用内容的,没有进行事先定义(一般用来进程间通信)。

3. 进程对信号的处理

我们可以使用signal函数或sigaction函数来进行信号处理函数的注册和绑定。这包括注册事先未被定义的信号或修改已经被定义的信号。

signal函数使用方法
在这里插入图片描述

//头文件包含
#include 
//函数用法
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
//用法举例
#include 
#include 
#include 

typedef void (*sighandler_t)(int);

void func(int sig)
{
	if (SIGINT != sig)
		return;
	//设计一个返回信号值的函数
	printf("func for signal: %d.\n", sig);
}

int main(void)
{
	//这里指定信号值为2,相当于把Ctrl^C绑定为了信号处理
	sighandler_t ret = (sighandler_t)-2;
	signal(SIGINT, func);
	//signal(SIGINT, SIG_DFL);				// 指定信号SIGINT为默认处理
	//ret = signal(SIGINT, SIG_IGN);		// 指定信号SIGINT为忽略处理
	if (SIG_ERR == ret)
	{
		perror("signal:");
		exit(-1);
	}
	
	printf("before while(1)\n");
	while(1);
	printf("after while(1)\n");
	
	return 0;
}

在这里插入图片描述
signal函数绑定一个捕获函数后信号发生后会自动执行绑定的捕获函数,并且把信号编号作为传参传给捕获函数。
signal的返回值在出错时为SIG_ERR,绑定成功时返回旧的捕获函数。

signal函数的优点和缺点
(1)优点:简单好用,捕获信号常用
(2)缺点:无法简单直接得知之前设置的对信号的处理方法

sigaction函数使用方法
sigaction比signal好的一点:sigaction可以一次得到设置新捕获函数和获取旧的捕获函数(其实还可以单独设置新的捕获或者单独只获取旧的捕获函数),而signal函数不能单独获取旧的捕获函数而必须在设置新的捕获函数的同时才获取旧的捕获函数。

//函数用法
 int sigaction(int signum, const struct sigaction *act,
                     struct sigaction *oldact);
//1.如果想要获取旧的捕获函数
rtn = sigaction(signum, NULL, oldact);
//2.如果想要获取新的捕获函数
rtn = sigaction(signum, newact, NULL);
//3.如果替换驳捕获函数
rtn = sigaction(signum, newact, oldact);

利用alarm和pause来模拟sleep
因为alarm的作用是定时,然后到时间产生信号pause的作用是将内核挂起(暂停)。两者相结合即可实现挂起内核,然后到时间产生信号(唤醒暂停态)使得程序继续运行。

pause函数的作用就是让当前进程暂停运行,交出CPU给其他进程去执行。当当前进程进入pause状态后当前进程会表现为“卡住、阻塞住”,要退出pause状态当前进程需要被信号唤醒。

#include 
#include 			// unix standand
#include 


//定义了一个空函数
void func(int sig)
{
}

void mysleep(unsigned int seconds);

int main(void)
{
	printf("before mysleep.\n");
	mysleep(3);
	printf("after mysleep.\n");
	return 0;
}

void mysleep(unsigned int seconds)
{
	struct sigaction act = {0};
	//将函数与结构体内部的函数指针元素进行绑定
	act.sa_handler = func;
	//对信号处理函数进行注册
	sigaction(SIGALRM, &act, NULL);
	
	alarm(seconds);
	pause();
}

你可能感兴趣的:(Linux嵌入式)