进程

一.进程相关概念

1.进程的概念

程序的一次执行过程,内核中担当分配系统资源(CPU时间,内存)的实体

2.pcb(进程控制块)

程序运行的描述,pcb是进程属性的集合,在Linux中描述进程的结构体叫做task_struct

task_ struct内容分类

标识符: 描述本进程的唯一标示符,用来区别其他进程
状态: 任务状态,退出代码,退出信号等
优先级: 相对于其他进程的优先级
程序计数器: 程序中即将被执行的下一条指令的地址
内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
上下文数据: 进程执行时处理器的寄存器中的数据
I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表
记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。

3.进程状态

R运行状态(running): 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里;运行状态的进程才会被操作系统调度在CPU运行

S可中断休眠(sleeping): 进程在等待事件完成,当前的阻塞能被中断唤醒

D磁盘休眠状态(Disk sleep)有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束,当前的阻塞不能被中断唤醒,这个状态主要是为了保证内核的某些处理流程不被打断

T停止状态(stopped): 停止运行,什么也不做;可以通过发送 SIGSTOP 信号来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行

Z僵死状态(zoombie):进程已经退出但是资源没有完全被释放,是一种等待后续处理的状态

X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态。

t追踪状态(Traced):进程暂停下来,等待其他进程对它进行操作,比如gdb调试时就会处于追踪状态

4.僵尸进程和孤儿进程

(1)僵尸进程

但子进程先于父进程退出,并且父进程没有读取到子进程退出时的返回代码就会产生僵尸进程
僵尸进程会以僵死态保持在进程表中,并且会一直在等待父进程读取退出状态代码。
进程退出不再运行,但是资源没有完全被释放

僵尸进程的危害:内存泄漏

僵尸进程如何处理和避免:
让父进程退出,但是太粗暴了;
进程等待;

(2)孤儿进程

父进程先于子进程退出,子进程会被1号init进程领养,子进程会变成孤儿进程

孤儿进程不会成为僵尸进程,因为1号进程一直在关注子进程的退出状态

守护进程(精灵进程)

守护进程是一种特殊的孤儿进程,运行在后台,但是与终端和登陆会话脱离关系,不再受影响。
通常是一种运行在系统后台的批处理程序

创建精灵进程的接口:
int daemon(int nochdir, int noclose);

5.环境变量相关命令

环境变量的作用:
指定操作系统运行环境的一些参数;
可以进行小量的父子进程间通信;

操作命令:
echo: 显示某个环境变量值
export: 设置一个新的环境变量
env: 显示所有环境变量
unset: 清除环境变量
set: 显示本地定义的shell变量和环境变量

env:查看环境变量
使用示例:
在这里插入图片描述

set:查看变量
使用示例:
在这里插入图片描述

echo:显示变量信息
在这里插入图片描述

PATH:加入搜索路径
格式:
PATH=${path}:要加入的搜索路径
使用示例:
将当前路径加入搜索路径之后,直接输入可执行文件名称就可以运行程序了
在这里插入图片描述export:将变量设置为环境变量
格式:
export 变量名
使用示例:
在这里插入图片描述

unset:撤除环境变量
格式:
unset 变量名
使用示例:
在这里插入图片描述


二.进程控制

1.进程产生

fork

函数原型:pid_t fork(void);
作用:用已存在进程创建一个新进程,已存在进程作为父进程,新进程作为子进程
fork后立即创建虚拟地址空间,只是物理内存没有立即开辟,只有数据发生改变时才会为子进程开辟物理内存,并将数据拷贝到新的物理内存

返回值:出错返回-1,子进程返回0,父进程返回子进程pid(大于0)

vfork

(1)函数原型
pid_t vfork(void)
(2)作用
通过复制父进程创建一个子进程,父子进程公用同一块虚拟空间。
创建子进程后,父进程会阻塞直到子进程exit退出或者(程序替换)之后才会运行。
(3)为什么父子进程不能同时进行?
公用同一个虚拟地址空间,同时运行会造成栈混乱;
举个例子:
main函数进入函数调用栈之后,父进程先运行printf函数,printf函数入栈,时间片完后再切换到子进程运行strcpy函数,strcpy函数入栈,然而strcpy函数运行了一段时间后,时间片完,继续切换到父进程,这时再次调用printf函数,那么strcpy函数就要出栈,函数栈就会发生混乱。
图示如下:
在这里插入图片描述

(4)需要注意的点:
vfork创建的子进程不能在main函数中return退出,但可以使用exit退出。
原因如下:
因为return退出不仅仅释放的是子进程的资源,父进程和子进程共用资源,父进程的资源也被释放,那么父进程也被迫退出。而exit仅仅退出的是子进程,释放的是子进程自己的PCB,父进程并不会受到影响。

代码示例:

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
//vfork创建的子进程先于父进程先运行
int main()
{
printf("Good Morning!Linux\n");
pid_t pid=vfork();
if(pid==-1)
        printf("创建子进程失败\n");
else if(pid==0)
{
        printf("这是子进程\n");
        //return 1;//子进程使用return,最后会显示segmentation fault
        exit(0);
}
else
{
        printf("这是父进程\n");
}
return 0;
}

运行结果:
在这里插入图片描述

写时拷贝

什么是写时拷贝技术?
当父进程创建子进程时,父进程与子进程各自拥有自己的虚拟地址空间,但他们数据映射的是同一块物理内存,等待内存发生改变的时候,为子进程重新开辟一块独立的的物理地址空间,保存子进程的数据(保证进程的独立性)

为什么不直接为子进程开辟一块独立的空间,而是等内存改变的时候开辟?
当子进程只是进行读操作时,为子进程开辟的空间会浪费;而当内存发生改变时,不得不为子进程申请一块独立的空间时,再开辟空间,这样可以提高fork创建子进程的效率,节省资源。

2.进程终止

(1)进程退出的三种方式

1.return退出-----在main函数中使用
会释放所有的资源,刷新文件缓冲区
注意:
vfork创建的子进程不能使用return退出,原因是会释放父子进程公用的资源,父进程也被迫退出

2.调用库函数exit退出------可以在任意位置使用
会释放当前进程的资源,刷新文件缓冲区

3.调用系统接口_exit退出------可以在任意位置使用
直接退出,不刷新文件缓冲区

(2)三种方式退出的代码演示

return退出:
代码:

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int main()
{
printf("你好,");     //\n具有刷新缓冲区的作用,如果\n存在,就先实现你好,再睡眠3秒
sleep(3);
printf("This is a good time!");
return 0;
}

运行结果:
先睡眠三秒,再打印 你好,I like you,郑玥
分析:因为return在退出进程之前会将文件缓冲区的内容进行刷新

调用库函数exit退出
代码演示:

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
void func()
{
	exit(0);
}
int main()
{
	printf("你好,");     //\n具有刷新缓冲区的作用,如果\n存在,就先实现你好,再睡眠3秒
	sleep(3);
	func();
	printf("This is a good time!");
	return 0;
}

运行结果:
先睡眠三秒,再打印一句 你好,
然后退出
分析:exit会在退出前刷新文件缓冲区的内容,先将文件缓冲区的内容送到控制台打印,exit后面的代码就不会执行

调用系统接口_exit退出
代码演示:

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
void func()
{
	_exit(0);
}
int main()
{
	printf("你好,");     //\n具有刷新缓冲区的作用,如果\n存在,就先实现你好,再睡眠3秒
	sleep(3);
	func();
	printf("Hello world\n");
	return 0;
}

运行结果:
先睡眠三秒,再退出,不打印任何内容
分析:_exit接口直接退出,并不刷新文件缓冲区的内容,所以没有任何输出

(3)进程退出的返回值

有两种情况:
1.正常退出:以上三种进程退出方式
返回值用一个字节来表示,范围为0~255
如果返回值为-1,等同于255
如果返回值为258,等同于2
可以用echo $?查看上一次程序执行的返回值

2.异常退出:程序因为某些错误异常崩溃而退出
这种情况下返回值没有意义

3.进程等待

(1)进程等待所做的工作

等待子进程的退出,获取子进程的返回值,释放子进程的资源,
避免产生僵尸进程。

僵尸进程是由于子进程退出,但父进程没有读取到子进程的退出代码,进而无法释放子进程的资源,造成操作系统失去了对这块内存的控制权。

僵尸进程可以通过进程等待来避免

(2)进程等待的两种实现函数
wait

1)函数原型:
pid_t wait(int* status)
参数:
status:等待进程退出时的返回值等一系列信息,不关心返回值可以使用NULL
status共有四个字节大小,其中低16位的高八位存放的是子进程退出的返回值,低16位的低八位的最高位存放一个标志位core dump(表示程序异常退出是否需要核心转储),低7位存放异常退出信号值(全为0说明正常退出,否则非正常退出),status右移八位再和0x7f相与就是子进程退出的返回值

返回值:成功返回终止的子进程的pid,失败返回-1

2)作用:
阻塞等待任意一个子进程的退出,获取返回值
3)使用示例:
wait(NULL):阻塞等待任意一个子进程的退出

waitpid

1)函数原型:
pid_t waitpid(int pid,int* status,int options)

参数:
pid表示等待的进程id,-1表示任意进程

status:等待进程退出时的返回值等一系列信息,不关心返回值可以使用NULL
status共有四个字节大小,其中低16位的高八位存放的是子进程退出的返回值,低16位的低八位的最高位存放一个标志位core dump(表示程序异常退出是否需要核心转储),低7位存放异常退出信号值(全为0说明正常退出,否则非正常退出),status右移八位再和0x7f相与就是子进程退出的返回值

option表示等待方式,0表示阻塞等待,WNOHANG表示非阻塞等待

返回值:
成功返回终止的子进程的pid,失败返回-1
2)两种等待方式
阻塞
一直等待,直到子进程退出
非阻塞
每隔一段时间,去查看一下子进程是否退出

3)使用示例:
waitpid(-1,NULL,0)
以阻塞方式等待任意一个子进程退出

waitpid(pid,NULL,0)
以阻塞方式等待进程号为pid的子进程退出

waitpid(pid,NULL,WNOHANG)
以非阻塞方式等待进程号为pid的子进程退出

(3)进程等待代码演示
#include<stdio.h>       //printf
#include<unistd.h>      //fork sleep
#include<stdlib.h>      //exit

int main()
{
pid_t pid=fork();
if(pid==0)
{
//子进程先休眠5秒,再退出
//父进程什么都不做,那么子进程就会变为僵尸进程
sleep(5);
exit(1);
}

/*
//父进程阻塞等待子进程退出
//wait(NULL);
//waitpid(-1,NULL,0);   //作用等同于上一行的wait语句,以阻塞方式等待所有的子进程退出
//waitpid(pid,NULL,0);  //以阻塞方式等待自己的子进程退出
int res;
//以非阻塞方式等待自己的子进程退出,子进程没有退出就返回
while((res=waitpid(pid,NULL,WNOHANG))==0)
{
printf("现在子进程还没有退出,你先等待一会!\n");
sleep(1);
}
*/

int res,status;
//以非阻塞方式等待自己的子进程退出,子进程没有退出就返回
while((res=waitpid(pid,&status,WNOHANG))==0)
{
printf("现在子进程还没有退出,你先等待一会!\n");
sleep(1);
}

/*
//判断status的低七位是否为0,0代表有返回值
if((status&0x7f)==0)
{
//返回的status本来只是存储在一个字节中
//但是将它又放在了两个字节的空间的高八位,所以要用本来的返回值*(2^8)
//左移了八位,如果子进程退出返回值为1,就会显示为256,如果为255,就会显示为65280
printf("status:%d\n",status);
printf("status>>8:%d\n",(status>>8)&0xff);
}
*/

//这个宏的作用等同于上面的语句块,用来判断status的低七位是否为0,0代表有返回值
if(WIFEXITED(status))
{
printf("status:%d\n",WEXITSTATUS(status));
}

while(1)
{
        printf("我正在打麻将,不要烦我!\n");
        sleep(1);
}
return 0;
}

程序运行结果:
在这里插入图片描述

4.进程替换

(1)进程替换的本质

更新 PCB中的内容,改变PCB对应的程序

(2)进程替换的接口

int execve(const char *filename, char *const argv[],
char *const envp[]);
设置参数数组和环境变量数组来替换程序

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

1)execve是系统调用接口,其余6个都是库函数

2)l和v的区别
l表示需要传递不定参,
v表示传递的是一个形参数组

3)有p和没有p的区别:
在于是否需要带路径,有p不需要带路径,没有p需要带路径,默认去PATH环境变量指定的路径下去找

此处可以使用export

4)有e和没有e的区别
在于是否需要初始化环境变量,有e表示需要初始化,没有e表示不需要

(3)进程替换代码演示

param.c经过编译后会在程序替换中用到
param.c

#include<stdio.h>
int main(int argc,char* argv[],char* env[])
{
//argc:运行参数的个数
//argv:当前的运行参数
int i;
for(i=0;argv[i]!=NULL;++i)
{
        printf("argv[%d]=%s\n",i,argv[i]);
}
for(i=0;env[i]!=NULL;++i)
{
        printf("env[%d]=%s\n",i,env[i]);
}
return 0;
}

replace.c

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>

int main()
{
	char* params[] = {"1", "0", "2", "4", NULL};
	char* envs[] = {"Hello", "world", "good", "morning", NULL};

	pid_t pid = fork();
	if(pid < 0)
	{
		perror("fork error\n");
		exit(-1);
	}
	else if(pid == 0)
	{
		printf("子进程要进行进程替换了,替换为param.c\n\n\n\n\n\n\n\n");

		/*
		 int execve(const char *filename, char *const argv[],
		                   char *const envp[])
		*/
		//execvp:参数需要传一个数组,环境变量也需要传一个数组,两个数组的最后一个成员必须是NULL
		//execve("./param", params, envs);

		//int execl(const char *path, const char *arg, ...);
		//execl:指定路径和参数,参数采用不定参,最后一个参数必须是NULL
		//execl("./param", "000", "111", "222", NULL);

		//int execlp(const char *file, const char *arg, ...);
		//execlp:有p说明不需要带路径,但是前提是当前的环境路径将要替换程序的环境路径包含了
		//环境变量的设置:PATH=${PATH}:路径
		//execlp("param", "1", "2", "3", NULL);

		/*
	 	int execle(const char *path, const char *arg,
	        	           ..., char * const envp[]);
		*/
		//指定路径,参数不定参,环境变量是数组
		//execle("./param", "111", "222", NULL, envs);

		//int execv(const char *path, char *const argv[]);
		//指定路径,参数是数组,环境变量是系统的环境变量
		//execv("./param", params);


		//int execvp(const char *file, char *const argv[]);
		//指定可执行文件名,参数采用数组形式,会在系统的环境变量中找文件所在路径,所以必须将文件所在路径写入环境变量
		//execvp("param", params);


		/*
		int execvpe(const char *file, char *const argv[],
		                   char *const envp[]);
		*/
		//指定文件名,参数和环境变量都采用数组形式
		execvpe("param", params, envs);

		printf("子进程进程替换成功\n");
		exit(2);
	}
	
	wait(NULL);
	printf("父进程等待子进程退出成功!\n");

	return 0;
}


程序运行结果:
在这里插入图片描述


三.进程间通信

1.进程间无法直接通信的原因

因为每个进程有自己独立的虚拟地址空间,访问都是自己的虚拟地址,具有独立性,因此无法直接通信。

2.进程间通信的本质

操作系统为进程提供传输媒介,进程间通过传输媒介进行交互

3.通信方式的种类

因为不同的应用场景,所以有各种各样的进程间通信方式,大致分为
管道,共享内存,消息队列,信号量,信号,套接字

4.各种通信机制的详细介绍

(1)管道
1)什么是管道?

管道是进程之间的一种单向数据流,一个进程写入管道的所有数据都由内核流向另一个进程,另一个进程由此可以从管道中读取数据。

管道是操作系统为进程在内核中开辟的一块缓冲区(内存),用于数据的传输;

2)管道的分类:
a.匿名管道

没有标识符的管道,只能用于亲戚进程间通信,通过子进程拷贝父进程中的信息,来访问同一块内核缓冲区,进而进行数据的传输。
创建匿名管道的接口:
int pipe(int pipefd[2]);
参数:
一个输入输出型参数,大小为2的整型数组,其中下标为0处存储用于读的文件描述符,下标为1处存储用于写的文件描述符;
返回值:
成功返回0,失败返回-1

匿名管道使用演示:
pipe.c:

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

int main()
{
    int pipe_fd[2];
    //匿名管道文件的创建必须在创建进程之前
    int re_pipe=pipe(pipe_fd);
    if(re_pipe < 0)
        perror("pipe error!");
    //创建子进程
    pid_t pid=fork();
    if(pid<0)
        perror("fork error");
    else if(pid==0)
    {
        char* data="I am so handsome!";
        write(pipe_fd[1],data,strlen(data));
    }
	else
    {
        char buf[1024]={0};
        read(pipe_fd[0],buf,1023);
        printf("父进程读取到了管道中的数据:%s\n",buf);
    }

    return 0;

}

运行结果:
在这里插入图片描述

b.命名管道

有标识符的管道,适用于同一主机任意进程间通信。
所有进程都可以通过标识符找到这块内核中的缓冲区,访问这块缓冲区,进而进行信息的交互。

命名管道的创建和使用:
创建:mkfifo 管道文件名称
示例:

mkfifo testfifo

使用接口:
int mkfifo(const char *pathname, mode_t mode);
参数:
pathname:管道文件的路径
mode:设置对于管道的操作权限
返回值:
成功返回0,失败返回-1;

命名管道基本使用代码演示:
可以使用mkfifo创建一个命名管道testfifo,或者在程序中使用mkfifo函数创建管道文件,创建好之后就可以像使用普通文件一样对管道文件进行打开,并向其中写入数据、从中读取数据。

mkfifo testfifo

read.c:

#include<stdio.h>
#include<unistd.h>
#include<errno.h>
#include<fcntl.h>

int main()
{
        umask(0);       //修改文件默认权限掩码
        //1. 创建管道文件
        int ret=mkfifo("./testfifo",0664);
        if(ret<0 && errno!=EEXIST)
        {       
                perror("mkfifo error");
                return -1;
        }

        //2. 打开管道文件
        int fd=open("./testfifo",O_RDONLY);
        if(fd<0)
        {       
                perror("open error");
                return -1;
        }
        
        printf("open successfully!\n");
        //3. 开始读数据
        while(1)
        {
                char buf[1024]={0};
                int res=read(fd,buf,1023);
                if(res<0)
                {
                        perror("read error");
                        return -1;
                }
                else if(res==0)
                {
                        printf("all write closed\n");
                        return -1;
                }
                printf("buf:%s\n",buf);
        }

        close(fd);

        return 0;
}
                       

write.c:

#include<stdio.h>
#include<unistd.h>
#include<fcntl.h>
#include<errno.h>
#include<string.h>

int main()
{
        umask(0);
        //1.创建管道文件
        int res=mkfifo("./testfifo",0664);
        if(res<0 && errno!=EEXIST)
        {
                perror("mkfifo error");
                return -1;
        }

		//2.打开管道文件
        int fd=open("./testfifo",O_WRONLY);
        if(fd<0)
        {
                perror("open error");
                return -1;
        }
        printf("open successfully\n");
 
        while(1)
        {
                char input[1024]={0};
                fgets(input,1023,stdin);
                //3.写入数据
                int ret=write(fd,input,strlen(input));
                if(ret<0)
                {
                        perror("write error");
                        return -1;
                }
        }

        close(fd);

        return 0;
}

运行结果演示:
写端:
在这里插入图片描述

读端:
在这里插入图片描述

3)管道的特性

a.半双工通信:两个进程间可以进行双向通信,同一时刻只能进行单向通信。
b.提供字节流传输服务:
数据传输是有序的,基于连接的,传输比较灵活;
具体体现:
数据先进先出;
所有读端关闭则write触发异常,所有写端关闭则read返回0不再阻塞
c.自带同步和互斥:
互斥:通过同一时间对临界资源的唯一访问保证访问操作的安全性
同步:通过条件判断对临界资源进行更加有序以及合理的访问。
具体体现:
同步:对管道进行写入操作,大小不超过PIPE_BUF(4096个字节)大小则保证原子性。
互斥:管道中没有数据则read阻塞;管道中数据满了则write会阻塞
d.生命周期随进程
当没有进程使用这块管道空间时,管道生命周期结束。

(2)共享内存
1)什么是共享内存?

共享内存是操作系统为进程开辟的一块物理内存,多个进程将这块物理内存映射到自己的虚拟地址空间中,通过自己的虚拟地址访问,进而实现数据共享
但是共享内存不具有安全性,所有和这块共享内存建立映射的进程都可以修改这块共享内存中的数据。
如果主动删除一块共享内存,共享内存并不会立即被操作系统回收,而是等待所有和这块共享内存建立映射关系的进程都不再使用这块共享内存时才会真正销毁和回收,但在这段等待时间内,共享内存不会接收新的映射的建立

2)操作流程及代码演示:

a.操作流程
第一步,创建或打开共享内存
对应的函数:shmget
函数原型:
int shmget(key_t key, size_t size, int shmflg);
参数:
key:共享内存段标识符(a System V IPC key)
可以通过ftok函数获取,也可以自己设置为一个四字节的16进制数

size:共享内存大小

shmflg:由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的

返回值:
成功返回一个非负整数,即该共享内存段的标识码;失败返回-1

面试遇到的问题:多个进程如何访问同一块共享内存?
打开共享内存时使用相同的共享内存段标识符就可以打开同一块共享内存,共享内存打开成功后就会返回同一个共享内存标识符,可以通过相同的共享内存标识符操作同一块共享内存。


第二步,建立映射关系,将共享内存映射到进程的虚拟地址空间
对应的函数:shmat
原型:
void *shmat(int shmid, const void *shmaddr, int shmflg);
参数:
shmid: 共享内存标识
shmaddr:指定连接的地址
shmflg:它的两个可能取值是SHM_RND和SHM_RDONLY
返回值:
成功返回一个指针,指向共享内存首地址;失败返回-1

第三步,进行内存操作
可以使用strcpy,memcpy,read,write等接口实现

第四步,解除映射
对应的函数:shmdt
原型:
int shmdt(const void *shmaddr);
参数:
shmaddr: 共享内存首地址
返回值:
成功返回0;失败返回-1
注意:将共享内存段与当前进程脱离不等于删除共享内存段

第五步,释放共享内存空间
对应的函数:shmctl
原型:
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数:
shmid:由shmget返回的共享内存标识码
cmd:将要采取的动作(有三个可取值)
buf:指向一个保存着共享内存的模式状态和访问权限的数据结构
返回值:
成功返回0;失败返回-1

共享内存的声明周期是随内核的,如果共享内存使用结束后,没有在程序中释放共享内存,可以通过命令ipcrm -m shmid手动释放共享内存

b.演示
read.c:

#include<stdio.h>
#include<unistd.h>
#include<sys/shm.h>
#define IPC_KEY 0X12345678

int main()
{
        //创建共享内存
        //shmget(标识符,创建的共享内存大小,打开方式和权限)
        int shmid=shmget(IPC_KEY,1024,IPC_CREAT | 0664);
        if(shmid<0)
        {
                perror("shmget error!");
                return -1;
        }

        //建立映射关系
        //shmat(操作句柄,映射的首地址,访问方式)
        void* shm_start=shmat(shmid,NULL,0);
        if(shm_start==(void*)-1)
        {
                perror("shmat error");
                return -1;
        }
         //进行内存操作
        while(1)
        {
                //打印共享内存中的内容
                printf("%s\n",(char*)shm_start);
                sleep(1);
        }
        //解除映射关系
        shmdt(shm_start);

        //释放共享内存
        //shmctl(操作句柄,操作类型,信息结构)
        shmctl(shmid,IPC_RMID,NULL);

        return 0;
}

write.c:


#include<stdio.h>
#include<unistd.h>
#include<sys/shm.h>
#include<string.h>
#define IPC_KEY 0X12345678

int main()
{
        //创建或打开共享内存
        int shmid=shmget(IPC_KEY,1024,IPC_CREAT | 0664);
        if(shmid<0)
        {
                perror("shmget error");
                return -1;
        }

        //建立映射关系
        void* shm_start=shmat(shmid,NULL,0);
        if(shm_start == (void*)-1)
        {
                perror("shmat error");
                return -1;
        }
		//static int i=0;
		//进行内存操作
        while(1)
        {
                /*
                sprintf(shm_start,"this is process A  %d",i);
                i++;
                */
                char input[1024]={0};
                fgets(input,1023,stdin);
                strcpy(shm_start,input);
                sleep(1);
        }
        //解除映射
        shmdt(shm_start);
        //释放共享内存  
        shmctl(shmid,IPC_RMID,NULL);
        return 0;
}


运行结果展示:
写端:
在这里插入图片描述
读端:
在这里插入图片描述

3)特性

a.最快的进程间通信方式
因为访问的是物理内存,不涉及到内核操作,相较于其他方式少了两次数据拷贝操作

b.生命周期随内核
共享内存在物理内存开辟空间,将信息存储到内核,内核重启会释放或者用户自己手动释放

c.共享内存是不安全的
所有和共享内存建立映射关系的进程都可以对共享内存的内容进行修改,会出现某个进程修改或者覆盖其他进程写入共享内存的数据,因此是不安全的,通常搭配信号量来保证安全性

(3)消息队列
1)什么是消息队列?

内核中的一个优先级队列

2)原理

多个进程通过访问同一个消息队列,通过添加或者获取节点实现通信;
添加的节点的结构由用户自己定义,用户可以自己确定数据的长度和节点的类型,每个用户添加节点时节点类型都要不同,类型既是优先级判断基准也是身份判断标准。

3)操作流程

创建消息队列(在内核中创建一个优先级队列);
进程向消息队列中添加或者获取节点;
删除消息队列;

相关的接口:
msgget,msgsnd,msgrcv,msgctl

4)特性

自带同步与互斥;生命周期随内核;传输方式是数据块类型

(4)信号量
1)什么是信号量?

用于实现进程间同步和互斥的一种变量,主要用于控制
本质上是一个内核中的计数器+pcb等待队列。

2)实现同步和互斥的思路

同步的实现:
通过计数器对资源计数;
当计数>0才能访问资源,计数器-1;
当计数<=0不能访问资源,计数-1,将pcb状态置为可中断休眠状态,加入等待队列;
等到其他进程释放或者产生资源后,计数+1,如果此时计数>0,什么也不做,否则从pcb等待队列中唤醒一个pcb去获取资源

互斥的实现:
通过保证信号量资源计数不大于1来实现,
将计数器置为1,计数>0才能访问资源,访问资源时计数-1,释放资源后计数+1


(5) 进程信号
1.信号的概念

信号是事件发生时对进程的通知机制,有时也称之为软件中断。通过接收到的信号,通知进程发生了某个事件,打断进程当前操作,去处理这个事件。
是一种实现进程控制的进程间通信方式

2.信号的种类

Linux下有62种信号,可以通过kill -l 命令查看所有信号
其中1–31是非可靠信号,34–64是可靠信号

可靠信号和不可靠信号

可靠信号:
无论信号是否注册,都会注册一下,接收到几次信号就注册几次信号
非可靠信号:
不管信号是否注册,只会注册一下,不管接收到多少次信号只注册1次信号

3.信号的生命周期

分为:产生,注册,注销,处理

(1)产生

分为硬件产生和软件产生
1)硬件产生
通过键盘输入产生信号
例如:Ctrl+c(中断) ,Ctrl+z(停止) ,Ctrl+|(退出)
2)软件产生
通过kill命令,格式:kill -信号值 进程id,
如kill -9 1111
直接kill发送15( SIGTERM)信号

通过接口的调用发送信号
常用的接口:
kill,raise,abort,alarm
kill
给指定进程发送一个指定的信号
函数原型:
int kill(pid_t pid, int sig);
参数:
pid:进程号
sig:信号值
返回值:成功返回0,失败返回-1

raise
给进程自身发送一个指定的信号
函数原型:
int raise(int sig);
参数:
sig:信号值
返回值:
成功返回0,失败返回非0

abort
给进程自身发送一个SIGABRT信号,让进程终止
函数原型:
void abort(void);
无参数无返回值

alarm
设置一个时钟,seconds秒后给进程自身发送一个时钟信号
函数原型:
unsigned alarm(unsigned seconds);
参数:
seconds:提醒的时间(单位为s)
返回值:
如果有上一个alarm请求,并且还有剩余时间,将返回一个非零值,即直到上一个请求生成SIGALRM信号的秒数,否则返回0

信号产生接口使用代码演示:

  1 #include<stdio.h>
  2 #include<stdlib.h>
  3 #include<unistd.h>
  4 #include<signal.h>
  5 
  6 int main()
  7 {
  8   //kill(getpid(),SIGINT);    //让指定进程中断
  9   //raise(SIGINT);    //让进程自身中断
 10   //abort();      //让进程自身终止
 11   alarm(3);       //3秒后发送时钟信号                                                                                                   
 12   while(1)
 13   {
 14     printf("晚上也要敲代码\n");
 15     sleep(1);
 16   }
 17   return 0;
 18 }

运行演示:
在这里插入图片描述

(2)注册

在进程中注册一个信号,让进程知道自己收到了某个信号

具体做法:修改pending位图(未决信号集合)对应位为1,添加一个信号信息节点到sig_queue这个双向链表中

可靠信号和非可靠信号的注册
非可靠信号如果pending位图,对应位为0,就会添加一个sigqueue节点到sigueue链表中,注册一次,如果对应位为1,不会注册;

可靠信号收到n个信号,就注册n次,添加节点到sigqueue链表中,并且置pending位图对应位为1


知识点:
pending位图用来标记是否收到了某个信号,
sigqueue用来表示有多少信号

(3)注销

将信号进程的pcb中的一些信息移除(修改位图,删除节点)

为了让一个信号只会被处理一次,所以先注销后处理,先取出这个信号的信息,注销后去处理

非可靠信号:删除节点,将pending位图对应位置为0
可靠信号:删除信号节点,检查链表中是否还有相同节点,没有就修改pending位图对应位置为0

(4)处理

信号的处理也称为信号的递达,实际上就是打断进程当前的操作,去执行待处理信号对应的信号处理函数

知识点:
a.信号的处理方式
1)默认处理方式(SIG_DFL):操作系统定义的每个信号的处理方式
2)忽略处理方式(SIG_IGN):什么都不做
3)自定义处理方式
将操作系统定义的信号处理方式替换为用户自己定义的信号处理函数
函数模板:
typedef void(*sighandler_t)(int)

b.改变指定信号处理方式的接口signal
函数原型:
sighandler_t signal(int signum, sighandler_t handler);
参数:
signum:信号值
handler:自定义的信号处理函数
返回值:
成功返回上一个的信号处理函数的值
失败返回SIG_ERR

signal接口使用代码演示
代码:

  1 #include<stdio.h>
  2 #include<stdlib.h>
  3 #include<unistd.h>
  4 #include<signal.h>
  5 
  6 //自定义信号处理函数
  7 void sigcb(int signo)
  8 {
  9   printf("recv signo: %d\n",signo);
 10 }
 11 int main()
 12 {
 13   //将SIGINT信号的处理方式改变为自定义的信号处理方式                                                                  
 14   signal(SIGINT,sigcb);
 15   while(1)
 16   {
 17     printf("这是一个signal函数的测试,输入ctrl+c会有惊喜:\n");
 18     sleep(1);
 19   }
 20   return 0;
 21 }

运行演示:
在这里插入图片描述
c.自定义处理方式的信号捕捉流程
当一个进程产生,就进入用户态主控流程,当程序运行切换到内核态,在完成内核态功能后且即将返回用户态之前,会检查是否还有信号需要处理:
如果有信号,并且是内核态的信号处理函数,执行完对应的信号处理函数再返回用户态主控流程,
如果信号处理函数是自定义的信号处理函数,先返回用户态执行信号处理函数,通过sigreturn返回内核态,直至没有信号需要处理才返回用户态主控流程。

d.程序运行从用户态切换到内核态的几种方式
系统调用接口,中断,异常

e.信号的阻塞
信号阻塞,阻止信号被传达
注意:一个信号被阻塞后,依然收到这个信号会注册,但是暂时不做处理

1)知识点:
为了标记阻塞信号,就需要一个block位图来标记被阻塞的信号信息
2)阻塞信号的接口:sigprocmask
函数原型:
int sigprocmask(int how, const sigset_t *restrict set,
sigset_t *restrict oset);
参数:
how:操作类型
SIG_BLOCK —阻塞set集合的信号
SIG_UNBLOCK —将set集合的信号解除阻塞
SIG_SETMASK —将set集合中的信号设置为阻塞集合的信号

set:一个指向一组用于更改当前参数的信号阻塞集合的指针

oset:一个指向之前的信号阻塞集合的指针
返回值:
成功返回0,失败返回-1

3)sigprocmask接口使用代码演示:
预备知识:
清空集合:
int sigemptyset(sigset_t *set);

添加所有信号到set集合中:
int sigfillset(sigset_t *set);

添加signum信号到set集合中:
int sigaddset(sigset_t *set,int signum);

从set集合中移除signum信号:
int sigdelset(sigset_t *set,int signum);

判断signum信号是否在set集合中:
int sigismember(const sigset_t *set,int signum);

9号信号(SIGKILL)和19号信号(SIGSTOP)是不能阻塞的,也不能自定义这两个信号的处理方式,同样也不能忽略。这两个信号的处理方式是不能修改的

代码如下:

  1 #include<stdio.h>                                                                                                     
  2 #include<unistd.h>
  3 #include<stdlib.h>
  4 #include<signal.h>
  5 
  6 //自定义信号处理函数
  7 void ca_bc(int signo)
  8 {
  9   printf("recv signal:%d\n",signo);
 10 }
 11 int main()
 12 {
 13   //改变信号处理方式
 14   signal(SIGINT,ca_bc);   //非可靠信号
 15   signal(40,ca_bc);       //可靠信号
 16 
 17   sigset_t set;
 18   sigemptyset(&set);   //清空set集合
 19   sigfillset(&set);    //添加所有信号
 20 
 21   //阻塞所有信号
 22   sigprocmask(SIG_BLOCK,&set,NULL);
 
  	  //让程序运行停下来,在这期间给进程发送进程信号
 25   printf("输入回车,进入下一步:\n");
 26   //getchar:等待回车
 27   getchar();
 28   printf("解除信号阻塞,查看结果\n");
 29 
 30   //解除阻塞,查看信号处理结果
 31   sigprocmask(SIG_UNBLOCK,&set,NULL);
 32   
 33   //死循环查看结果
 34   while(1)
 35   {
 36     sleep(1);
 37   }
 38 
 39   return 0;                                                                                                           
 40 }


运行演示:
在这里插入图片描述
在这里插入图片描述
4)一个进程无法被杀死的原因
该进程为僵尸进程;
进程处于停止状态;
信号被阻塞或者忽略;
杀死进程的信号的信号处理函数被更改为自定义信号处理函数;

4.常见的两种信号
SIGCHLD信号

一个子进程退出后,给父进程发送的子进程状态改变信号,但SIGCHLD的默认处理方式是什么都不做,所以会产生僵尸进程。

要想避免僵尸进程,需要在父进程中等待子进程退出,但是父进程一直等待会浪费资源;
如果不想等待,可以使用信号解决。可以自定义SIGCHLD信号的处理方式,在信号的处理方式中调用等待进程退出的接口

a.注意:
SIGCHLD是一个非可靠信号,意味着多个子进程同时退出,有可能事件丢失。所以有一种更好的办法就是将SIGCHLD的处理方式设置为SIG_IGN,通过显示忽略,让子进程退出时自动释放资源。

b.SIGCHLD避免僵尸进程使用演示
代码:

  1 #include<stdio.h>                                                                                                     
  2 #include<stdlib.h>
  3 #include<unistd.h>
  4 #include<signal.h>
  5 #include<sys/wait.h>
  6 
  7 //自定义信号处理函数
  8 void sig_cb(int signo)
  9 {
 10   printf("signo:%d\t有子进程退出\n",signo);
 11   wait(NULL);     //阻塞等待任意一个子进程退出
 12 }
 13 
 14 int main()
 15 {
 16   //改变信号处理方式,调用自定义处理函数
 17   signal(SIGCHLD,sig_cb);
 18   //signal(SIGCHLD,SIG_IGN);    //显示忽略,通知操作系统什么也不做,子进程退出时释放资源
 19   pid_t pid=fork();
 20   if(pid<0)
 21   {
 22     perror("fork error");
 23     return -1;
 24   }
  	  else if(pid==0)
 26   {
 27     sleep(4);
 28     exit(0);    
 29   }
 30   else
 31   {
 32     while(1)
 33     {
 34       printf("中午是敲代码的好时间\n");
 35       sleep(1);
 36     }
 37   }
 38   return 0;
 39 }


运行演示:
在这里插入图片描述

SIGPIPE信号

管道所有读端被关闭,写端触发异常退出发送的信号
SIGPIPE信号默认处理方式是退出进程,如果不想退出需要自定义或者显式忽略

使用演示:
关键代码:
在这里插入图片描述

运行演示:
在这里插入图片描述

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值