进程与线程以及进线程间通信

目录

进程

1.进程的基本概念

2.进程的特性

3.进程的类型

4.进程和程序的区别

5.进程的几种状态

6.进程的内存结构

进程编程

1.进程编程基础

fork()函数

exec函数族

exit和_exit函数

wait和waitpid函数

2.Linux守护进程

1.概述

2.编写守护进程

线程

线程基本编程

1.pthread_create函数

2.pthread_exit函数

3.pthread_join函数

4.pthread_cancel函数

5.pthread_detach 线程分离函数 (使线程不受其他线程影响)

6.pthread_self   获取线程ID

线程同步与互斥

1.互斥锁

2.信号量线程控制

进程间通信

传统通信

1.无名管道

2.有名管道

3.信号

IPC通信

1.共享内存

2.消息队列

3.信号灯集

信号灯集的创建

信号灯的操作函数

信号灯集的控制函数


进程

1.进程的基本概念

进程是指一个具有独立功能的程序在某个数据集合上的一次动态执行过程,是操作系统进行资源分配和调度的基本单元,一次任务的运行可以激活多个进程,相互合作完成目标。

2.进程的特性

1.并发性。多个进程可以同时并发执行互不干扰

2.动态性,进制有一个完整的生命周期(创建,调度,消亡),他的状态是不断变化的,且具有动态的地址空间。

3.交互性。可能在执行过程中与其他进程发生通信。

4.独立性。进程是一个相对完整的资源分配和调度的基本单元,各个进程的地址空间是相互独立的,只有采用特定的通信机制才能实现彼此之间的通信

3.进程的类型

1.交互式进程

2.批处理进程。

3.守护进程。一直在后台运行,和任何终端都不关联,在系统启动时开始执行,系统关闭时才结束

4.进程和程序的区别

程序是一段静态的代码,是有序指令的集合,没有任何执行的概念

进程是一个动态的概念,他是程序一次执行过程,包括创建调度执行和消亡的整个过程。当进程结束,所有资源被操作系统自动回收。

5.进程的几种状态

1.运行状态,标志符R ,+表示前台运行

2.等待态,S:可中断(软件开中断)D:不可中断(软件不可中断但硬件可以,这种状态一般表示与硬件进行交互),表示进程进入资源等待过程,例如getchar()等待终端输入。

3.停止态:T表示停止,进程处于休眠的状态,需要通过特定信号将其唤醒,可以通过信号使进程进入停止态例如CTRL Z。

4.僵死态:Z,表示子进程结束,父进程未退出,并且未使用wait函数族等系统调用来回收子进程的退出状态,资源未被回收。

5.消亡状态。

注:当前进程号PID和父进程号PPID的系统调用函数分别为getpid()和getppid()

6.进程的内存结构

Linux系统用的是虚拟内存管理技术,每个进程都有独立的地址空间。该地址空间大小为4GB的线性虚拟空间,又分为用户空间与内核空间,用户空间是0-3GB内核空间占据3-4GB.只有用户进程在使用系统调用时才可以访问内核空间,它由内核负责映射,是固定的不会随着进程改变。

 用户空间一般包括以下几个区域:

1.只读段。具有只读属性,包含程序代码和只读数据

2.数据段。存放全局变量和静态变量。分为初始化数据段(.data)和未初始化数据段(.bss)分别存放初始化和未初始化的全局和静态变量.

3.栈。系统自动分配释放,存放函数的参数值,局部变量的值,返回地址等。

4.堆。存放动态分配的数据,由程序员分配释放,或者程序结束系统自动回收。

5.共享库的内存映射区域。这是Linux动态链接器和其他共享库代码的映射区域。

进程编程

1.进程编程基础

fork()函数

fork函数在Linux中是一个非常重要的函数,因为它虽然执行一次却返回两个值!!它的作用是从已存在的进程中创建一个新的进程,称为子进程,原进程我们称之为父进程。使用fork函数我们得到的子进程起始是父进程的一个复制品,他会把父进程的地址空间都复制过来,包括上下文,代码段进程堆栈,内存信息等,而子进程独有的只有它的进程号,资源使用和计时器。

它们有一个重要的区别,父进程中的返回值是子进程的进程号pid,而子进程返回0,通过这个判断父进程和子进程。

注意:子进程没有执行fork函数,而是从fork函数调用的下一句语句开始执行。

fork函数语法要点
所需头文件

#include <sys/types.h>

#include <unistd.h>

函数原型pid_t fork(void);
函数返回值0:子进程
子进程PID(大于0的整数):父进程
-1:出错
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <strings.h>
#include <unistd.h>
int g_n = 14;

int main()
{
	int n = 1;
	//printf("hello");  //被拷贝,所以父子进程都要输出.
	//printf("world\n");   //拷贝前已经被刷新,换行符使缓存被刷新
	pid_t pid = fork();
	if(pid == -1)
	{
		perror("fork");
		exit(-1);
	}
else if(pid == 0)//子进程
{
	n += 14;
	printf("child n=%d g_n=%d\n", n, g_n);
	printf("child &n=%p\n", &n); //父子进程中地址值一样,因为空间整体拷贝
}
else//父进程
{
	sleep(1);
	//printf("hello\n");
	printf("parent n=%d g_n=%d\n", n, g_n);
	printf("child &n=%p\n", &n);
}

//printf("hello\n");

return 0;

exec函数族

我们能否让fork函数创造的子进程帮我们执行一个新的程序呢,这样我们就可以同时执行两个不同的进程!!而exec函数就能帮我们完成,它根据指定的文件名或目录名找到可执行文件,并用它来取代当前进程的数据段,代码段和堆栈段,这里的可执行文件可以是二进制文件也可以是脚本文件。

int execl(const char *path, const char *arg, ...)

int execv(const char *path, char *const argv[]);

int execle(const char *path, const char *arg, ..., char *const envp[])

int execve(const char *path, char *const argv[], char *const envp[])

int execlp(const char *file, const char *arg, ...)

int execvp(const char *file, char *const argv[])    

 l: 代表后面的参数的传入是 list 形式(依次传入).

v: 代表后面的参数的传入是以指针数组传入的.  例如: char *arv[] = {"ls", "-l", NULL};

p: 第一个参数不需要指明路径,只需要传入可执行文件名就可以了. (在系统默认PATH路径下寻 找)

e: 最后一个参数可以传入环境变量.

 除了第一个参数以外后面参数所以形式都需要以NULL结尾!! 

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


int main(int argc, char *argv[])
{
	pid_t pid = fork();
	if(pid == -1)
	{
		perror("fork");
		exit(-1);
	}
else if(pid == 0)
{
	//if(-1 == execl("/bin/ls", "ls", "-l", ".", NULL))

	//if(-1 == execl("/home/farsight/mytest", "mytest", NULL))
	if(-1 == execlp("ls", "ls", NULL))
	{
		perror("execl");
		exit(-1);
	}
	exit(0);
}
else
{
	while(1)
	{
		//sleep(1);
		//printf("parent------\n");
		waitpid(-1, NULL, WNOHANG);
	}
	exit(0);
}

return 0;
}

exit和_exit函数

两个函数都是用来终止进程的。执行到这两个函数进程会无条件的停止剩下的所有的操作。但有一点区别。

void exit(int status)  

{   status: 结束状态.       EXIT_SUCCESS 或 0: 代表正常结束       EXIT_FAILURE 或 1 -1:代表错误结束      要处理缓存区内容(比如:把缓存剩下的内容写入文件).   }

void _exit(int status)  

{   status: 结束状态.       EXIT_SUCCESS 或 0: 代表正常结束 E     EXIT_FAILURE 或 1 -1:代表错误结束      不对缓存做任何处理,直接退出进程.   } 

wait和waitpid函数

wait() 与 waitpid()

{    

 等待子进程退出.并且回收子进程资源.    

 pid_t wait(int *status)    

 status: 获取进程退出状态.  不需要获取的话 就为 NULL    

 pid_t: 返回子进程的进程号      

为了避免僵尸进程的出现,通常会用wait来回收子进程的资源.    

       

 pid_t waitpid(pid_t pid, int *status, int options)      

pid: pid > 0 只等待进程ID等于pid的子进程,不管其它已经有多少子进程运行结束退出了只要指定的子进程还没有结束,waitpid就会一直等下去

  pid == -1 等待任何一个子进程退出,没有任何限制,此时waitpid和wait的作用 一模一样         pid == 0 等待同一个进程组中的任何子进程,如果子进程已经加入了别的进程组,waitpid不会对它做任何理睬                                  

 ps axj 可以查看组进程.                          

  pid < -1 等待一个指定进程组中的任何子进程,这个进程组的ID等于pid的绝对值    

status: 与wait一致          

 options: 为 0 与wait函数一样,需要一直等待进程退出(阻塞).            

为WNOHANG 不需要等待(不阻塞).      

 返回值: 正常返回子进程的进程号      

使用选项WNOHANG且没有子进程结束时:0      错误: -1

; }

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <strings.h>
int main(int argc, char *argv[])
{
	pid_t pid;
	if(-1 == (pid = fork()))
	{
		perror("fork");
		exit(-1);
	}
    else if(pid == 0)
    {
	    printf("child pid=%d ppid=%d\n", getpid(), getppid());
	    sleep(5);
	    printf("child eixt\n");
	    exit(0);
    }
    else
    {
	    while(1)
	    {
		    sleep(1);
		    printf("parent pid=%d---pid=%d\n", getpid(), pid);
		    if(-1 == waitpid(-1, NULL, WNOHANG))//不会发生阻塞
		    {
			    perror("waipid");
			    exit(-1);
		    }
	    }
	exit(0);
    }

return 0;
}

2.Linux守护进程

1.概述

守护进程是Linux中的后台服务进程。是一个生存期较长的进程,通常独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件,通常在系统启动时开始执行,在系统关闭时终止,一般的系统服务都是通过守护进程实现的。

2.编写守护进程

1.创建子进程,父进程退出(系统发现孤儿进程,会自动由1号进程init收养)

2.在子进程中创建新会话(setsid函数让进程拜托原会话的控制,摆脱原进程组的控制,摆脱原本控制终端的控制。)

3.改变当前目录(chdir函数)使用fork创建的子进程继承了父进程的当前工作目录,但在进程运行过程中,当前目录所在文件系统是不能卸载的,一般会让根目录作为守护进程的当前工作目录避免问题发生。

4.重设文件权限掩码:(屏蔽文件权限中的对应位)umask函数

5.关闭文件描述符:子进程从父进程那里继承一些已经打开的文件,这些文件占用了系统资源但可能不会被守护进程访问,也可能导致所在文件系统无法被卸载,特别是守护进程和终端无关,标准输出输入错误输出流应当被关闭。close()

代码如下:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <strings.h>
#include <fcntl.h>
#include <unistd.h>
void DaemonCreate(void)
{
	int n = 3;
	int i = 0;
	pid_t pid = fork();
	if(pid == -1)
	{
		perror("fork");
		exit(-1);
	}
	else if(pid > 0)//父进程关闭
		exit(0);
	if(setsid() == -1)//创建新会话
	{
		perror("setsid");
		exit(-1);
	}
	if(-1 == chdir("/"))//改变当前目录
	{
		perror("chdir");
		exit(-1);
	}
	umask(0);//重设文件权限掩码

	for(i = 0; i < n; i++)
	{
		close(i);//关闭文件描述符
	}

}

int main(int argc, char *argv[])
{
	DaemonCreate();
	int fd = open("err.log", O_WRONLY | O_CREAT, 0666);
	if(fd == -1)
	{
		exit(-1);
	}
	while(1)
	{
		sleep(1);
		if(-1 == write(fd, "errror msg\n", 11))
		{
			exit(-1);
		}
	}

	return 0;

}

线程

进程是系统中程序执行和资源分配的基本单位,那么线程就是程序执行的最小单位,又被称为轻量级进程,线程可以对进程的内存空间和资源进行访问并与同一进程的其他线程进行分享!一个进程可以拥有多个线程,线程自己不拥有系统资源,只拥有程序在运行中必不可少的(程序计数器,一组寄存器和栈),每个线程也都有各自的用户栈,核心栈和控制块等资源,但同一个进程中的各个线程共享着该进程所拥有的全部系统资源。

我们举一个例子来描述线程和进程之间的关系:如果把计算机的操作系统比作一个大的工厂,那么进程就是这个工厂中的各个相互独立的车间,线程则是车间中的流水线工人。每个车间中至少有一个工人,也可以有多个工人。这些“工人”共享着这个车间中的所有资源(访问其隶属进程的资源)。


线程基本编程

1.pthread_create函数

int pthread_create(pthread_t *thread, const pthread_attr_t *attr,  void *(*start_routine) (void *), void *arg);

{

thread: 线程ID,需要传递 变量的地址

attr:   设置线程属性参数. 如果选择默认属性则传递NULL  

start_routine: 是一个指针函数的指针 (要求参数void* 返回值也是void* 的函数) 传递一个线程函数.         

arg:  传递给线程函数的参数,如果不需要传参则 为NULL  

 返回值:  0表示成功         

                非0 表示错误(返回错误num)

}
 

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <strings.h>
#include <pthread.h>
#include <unistd.h>
void *MythreadFun(void *arg)
{
    while(1)
    {
        sleep(1);
        printf("thread1--------------------\n");
    }
    pthread_exit(NULL);//线程中不能用exit,他表示一个进程结束
}
int main(int argc, char *argv[])
{ 
    pthread_t tid1;
   if(0!= pthread_create(&tid1,NULL,MythreadFun,NULL))
   {
        perror("create");
        return -1;
   }
   while(1)//防止进程结束导致线程结束
   {
        sleep(1);
        printf("mainthread----\n");
   }
    return 0;
} 

2.pthread_exit函数

void pthread_exit(void *retval);

{  reatval: 设置线程退出状态. }

3.pthread_join函数

int pthread_join(pthread_t thread, void **retval);

{  thread: 线程ID  retval: 接收线程退出状态的参数. 不关心状态就为NULL    

4.pthread_cancel函数

int pthread_cancel(pthread_t thread);

 结束指定线程.  

5.pthread_detach 线程分离函数 (使线程不受其他线程影响)

线程可以分为: 结合属性 与 分离属性 .... 默认线程是结合属性  结合属性: 线程可能会相互之间造成影响(有一定联系) 分离属性: 使线程完全独立执行,相互之间不会造成影响,并且线程可以自动回收资源了

int pthread_detach(pthread_t thread)

6.pthread_self   获取线程ID

pthread_t pthread_self(void);

返回线程id 

线程同步与互斥

同步:使线程之间按照一定顺序来执行,也就是同步的概念一般通过信号量和条件变量来实现

互斥:对临界资源的一种保护,当线程访问该资源时,其他线程不能访问,一般通过互斥锁来实现,互斥锁只能锁定临界资源,不能锁定代码

1.互斥锁

互斥锁接口:

初始化锁

int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutex _ attr_t *mutexattr);    mutex: 锁变量的地址     mutexattr: 为NULL使用默认属性  

加锁:

int pthread_mutex_lock(pthread_mutex_t* mutex) 

解锁: 

int pthread_mutex_unlock(pthread_mutex_t* mutex)

删除锁:

int pthread_mutex_destroy(pthread_mutex_t* mutex)

返回值:0成功

-1出错

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <strings.h>
#include <pthread.h>
int m1=0;
int m2=0;
pthread_mutex_t mutex;

void *MythreadFun(void *arg)
{
	while(1)
	{
		pthread_mutex_lock(&mutex);
		++m1;
		++m2;
		pthread_mutex_unlock(&mutex);
		//usleep(5000);
	}
	pthread_exit(NULL);   //与return退出没有区别.
}

void *MythreadFun2(void *arg)
{
	while(1)
	{
		pthread_mutex_lock(&mutex);//加锁
		if(m1 == m2)
		{
			printf("相等---\n");
		}
		else
		{
			printf("不相等--- #%d\n", m1-m2);
		}
		//usleep(5000);
		pthread_mutex_unlock(&mutex);//解释
	}
	pthread_exit("thread2 quit");
}

int main(int argc, char *argv[])
{
	pthread_t tid1, tid2;
	char *quit_msg = NULL;

	pthread_mutex_init(&mutex, NULL);
	
	if(0 != pthread_create(&tid1, NULL, MythreadFun, NULL))//创建线程1
	{
		perror("create");
		return -1;
	}
	
	if(0 != pthread_create(&tid2, NULL, MythreadFun2, NULL))//创建线程2
	{
		perror("create2");
		return -1;
	}
	
	if(0 != pthread_join(tid1, NULL)) //阻塞函数,等待指定线程退出.
	{
		perror("join");
		return -1;
	}
	pthread_join(tid2, (void**)&quit_msg);//接收线程2的结束状态
	
	printf("%s\n", quit_msg);
	
	while(1)
	{
		sleep(1);
		printf("mainThread---------\n");
	}
	return 0;

}

2.信号量线程控制

信号量也是就操作系统中用到的pv操作,广泛运用在进程线程间的同步和互斥,他本质上是一个非负的整数计算,用来控制对公共资源的访问,如果用于互斥一般只需要设置一个信号量,而用于同步操作,往往设置多个信号量来控制他们之间的顺序执行,流程图如下图。

int sem_init(sem_t *sem, int pshared, unsigned int value);

初始化信号量:sem: 信号量类型变量的地址.  pshared: 选择信号量是工作在进程中还是线程中

线程中: 0 进程: 非0

 value: 设置信号量中value的初始值

 返回值:  0: 正确

 -1:错误

int sem_post(sem_t *sem);

释放资源 (生产产品) V操作 信号量资源+1    返回值:  0: 正确  -1: 错误 

int sem_wait(sem_t *sem); 

申请资源 (消费产品) P操作  信号量资源-1

 返回值:  0: 正确  -1: 错误

信号量为0时阻塞线程

案例:利用信号量实现同步

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <strings.h>
#include <pthread.h>
#include <semaphore.h>
int n = 0;
sem_t sem, sem1;//定义信号量

void *MythreadFun(void *arg)
{
	while(1)
	{
		sem_wait(&sem1);   //P操作
		n++;
		sem_post(&sem);    //V操作
			

	}
	pthread_exit(NULL);   //与return退出没有区别.

}

void *MythreadFun2(void *arg)
{
	while(1)
	{
		sem_wait(&sem);   //P操作
		printf("n=%d\n", n);	
		sem_post(&sem1);//V操作
	}
	pthread_exit("thread2 quit");
}

int main(int argc, char *argv[])
{
	pthread_t tid1, tid2;
	char *quit_msg = NULL;

	if(-1 == sem_init(&sem, 0, 0))
	{
		perror("sem");
		return -1;
	}
	
	if(-1 == sem_init(&sem1, 0, 1))
	{
		perror("sem");
		return -1;
	}
	
	if(0 != pthread_create(&tid1, NULL, MythreadFun, NULL))
	{
		perror("create");
		return -1;
	}
	
	if(0 != pthread_create(&tid2, NULL, MythreadFun2, NULL))
	{
		perror("create2");
		return -1;
	}
	
	if(0 != pthread_join(tid1, NULL)) //阻塞函数,等待指定线程退出.
	{
		perror("join");
		return -1;
	}
	pthread_join(tid2, (void**)&quit_msg);//接收线程2结束状态
	
	printf("%s\n", quit_msg);
	
	while(1)
	{
		sleep(1);
		printf("mainThread---------\n");
	}
	return 0;

}

进程间通信

进程间的通信方式主要有以下几种。

传统通信方式:无名管道、有名管道,信号

信号 IPC通信:共享内存、消息队列、信号灯集

BSD通信: 套接字通信

传统通信

1.无名管道

1、是"半双工"工作方式  "半双工"同时只能从一端写入,另一端读取.

2、无名管道不属于文件系统. 数据交互在"内核内存"完成,对它的读写可以使用普通的read 和write等函数.

3、无名管道只能用于亲缘进程间的通信. (父子进程和兄弟进程之间)

函数接口: pipe

int pipe(int pipefd[2]);

 用法: int fd[2];  pipe(fd);  

fd[0] 表示读端  fd[1] 表示写端 

通常先创建一个管道,再调用fork函数创建子进程,该子进程会继承父进程所创建的管道,确定读写端。

ret = read()在读取管道时(与读取文件返回0有区别). 如果写端关闭 且当中没有内容可读则会返回0. ret == 0

如果写端没有关闭 且当中没有内容可读,read会阻塞.

管道读写注意点如下:

1.读端如果关闭,写端的存在是没有意义的,所以内核会发送一个管道破裂信号(SIGPIPE),该信号会 默认结束进程 

2.当写端关闭,如果管道中还有内容则读端会继续读取,直到读取完成返回0. (向管道写入数据时,写进程就会试图向管道写入数据,如果管道缓存区满了,那么写操作将一直阻塞)

演示一下代码吧

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <strings.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
	int fd[2] = {0};
	if(-1 == pipe(fd))//判断出错
	{
		perror("pipe");
		exit(-1);
	}

	pid_t pid;
	if((pid = fork()) == -1)//判断进程
	{
		perror("fork");
		exit(-1);
	}
	else if(pid == 0)//子进程
	{
		close(fd[1]);//关闭写端
		char buf[32] = {0};//创建一个数组
		int ret = 0;
		while(1)
		{
			if((ret = read(fd[0], buf, sizeof(buf))) <= 0)//读管道内容
			{
				if(ret == 0)//管道没有内容可读
				{
					printf("pipe is empty\n");
				}
				else//读取出错
				{
					perror("read");//输出错误信息
					exit(-1);
				}
			}
			printf("recv:%s", buf);//输出从父进程读取的数据
		}
		exit(0);
	}
	else//父进程
	{
		close(fd[0]);//关闭父进程读段
		char buf[32] = {0};//定义数组
		int ret = 0;
		while(1)
		{
			printf("input:\n");
			fgets(buf, sizeof(buf), stdin);//输入数据
	
			if(-1 == write(fd[1], buf, strlen(buf)))//向管道写入内容
			{
				perror("write");
				exit(-1);
			}
		}
		wait(NULL);//回收子进程资源
		exit(0);
	}
	
	return 0;

}

2.有名管道

1、是"半双工"工作方式  "半双工"同时只能从一端写入,另一端读取.

2、有名管道属于文件系统.  数据交互在"内核内存"完成.

3、有名管道可以用于非亲缘进程间的通信. 

1.指令mkfifo 可以直接创建管道文件.或者mknod <管道名>在命令行创建。

2.接口函数: mkfifo() 创建管道文件.   int mkfifo(const char *pathname, mode_t mode);

pathname:要创建的管道名

mode:管道的访问权限

注意:为读打开的管道在open中设置参数O_RDNOLY,为写而打开的管道在open中设置参数为O_WRONLY,如果有一端设置为读写权限那么另一端操作就会出现问题哦千万注意!!

1.对于读进程,如果当前FIFO(管道)没有数据,读进程将一直阻塞到有数据写入或是FIFO写端都被关闭,且 读端如果关闭,写端的存在是没有意义的,所以内核会发送一个管道破裂信号(SIGPIPE),该信号会 默认结束进程 。

2.对于写进程,只要FIFO有空间,数据就可以被写入,若空间不足,写进程就会阻塞,直到数据都写入为止。且 当写端关闭,如果管道中还有内容则读端会继续读取,直到读取完成返回0. 

来看看代码演示吧:

读端的代码如下

//读端
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <strings.h>
#include <fcntl.h>
int main(int argc, char *argv[])
{
	if(mkfifo(argv[1], 0664) == -1)//判断创建管道文件成功与否
	{
		perror("mkfifo");
		return -1;
	}
	int r_fd = open(argv[1], O_RDONLY);//以读权限打开管道文件
	if(r_fd == -1)//判断错误
	{
		perror("open");
		return -1;
	}

	printf("fifo read success\n");//提示成功打开
	
	char buf[32] = {0};//数组存放读取数据
	
	while(1)
	{
		int ret = read(r_fd, buf, sizeof(buf));读取管道内容
		if(ret <= 0)//判断错误
		{
			if(ret == 0)//读完
			{
				printf("fifo is empty\n");
				break;
			}
			else
			{
				perror("read");//读取错误信息
				return -1;
			}
		}
		printf("recv:%s\n", buf);//输出读取内容
		memset(buf, 0, sizeof(buf));//清空数组内容以免残留
	}
	
	return 0;

}

写端的代码如下:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <strings.h>
#include <fcntl.h>
int main(int argc, char *argv[])
{
	int w_fd = open(argv[1], O_WRONLY);//以只写权限打开管道文件
	if(w_fd == -1)//判断失败
	{
		perror("open myfifo");//输出出错信息
		return -1;
	}

	printf("fifo write success\n");//提示打开成功
	
	char buf[32] = {0};
	while(1)
	{
		printf("input:");
		fgets(buf, sizeof(buf), stdin);//输入数据
		buf[strlen(buf)-1] = '\0';fgets会将\n也存入数组中,将\n变为\0
	
		if(write(w_fd, buf, strlen(buf)) == -1)//判断写入成功与否
		{
			perror("write");//错误信息打印
			return -1;
		}
		memset(buf, 0, sizeof(buf));//清空
	}
	return 0;

}

将读写端代码同时运行即可!!

3.信号

概念:在软件层面对中断的一种模拟方式.(软中断),内核进程与应用进程也可以使用信号来通 信. 查看信号种类命令:kill -l    一共62个信号,通常进程使用前31个

 进程处理信号的方式:

1.默认处理:几乎都是终止程序

2.忽略处理:不能忽略的信号: SIGKILL, SIGSTOP

3.自定义处理:使用函数来处理信号. 

信号发送函数:

kill()

int kill(pid_t pid,int sig)

pid:正数:发送信号给进程号为pid的进程

0:信号被发送到所有和当前进程在同一个进程组的进程

-1:信号发送给所有的进程表中的进程(除了进程号最大的进程外)

<-1:信号发送给进程组号为-pid的每一个进程

返回值:

成功:0

出错:-1

raise()只能进程向自身发送信号

int raise(int sig)

sig:信号类型

成功:0

出错:-1

alarm()

信号接收函数:

signal()

typedef void (*sighandler_t)(int);  

sighandler_t signal(int signum, sighandler_t handler);

signum: 信号值      handler: 处理函数.  SIG_IGN: 忽略   SIG_DFL: 默认处理      错误返回: SIG_ERR   
pause() 

 int pause(void);  

阻塞进程,等待任意信号来临.   返回值:错误 -1 

看看代码:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <strings.h>
#include <signal.h>
#include <sys/types.h>
int main()
{
int n = 20;
if(SIG_ERR == signal(SIGINT, SIG_IGN))
{
	perror("signal");
	return -1;
}
while(n--)
{
	sleep(1);
	printf("hello\n");
	if(n == 10)
	{
		if(-1 == kill(getpid(), SIGKILL))
		{
			perror("kill");
			return -1;
		}
	}
}

return 0;
}

IPC通信

1.共享内存

概念:内核会寻找一片内存空间,然后将空间进行映射操作,能够把内存映射到用户空间。大大提 升了通信效率. 

操作流程如下:

1. 创建共享内存空间 (内核里)

2. 映射内核空间 到 用户空间

3. 读/写该共享内存

4. 取消映射、删除共享内存 

注:ipcs -m :查看共享内存

ipcrm -m  id: 删除共享内存    例如: ipcrm -m 1234

key_t ftok(const char *pathname, int proj_id);

获取key值:标识共享内存,以及用于区分共享内存用于亲缘 还是 可以用于非亲缘.  

 pathname: 文件名   proj_id:  组合成key 需要用到的id  

返回值:错误返回-1

正确返回 获取到的key值.  

key 值是通过两个参数(文件的节点号 与 proj_id的值)组合形成的 

int shmget(key_t key, size_t size, int shmflg);     

创建共享内存.    

key: IPC_PRIVATE 或 ftok的返回值 IPC_PRIVATE只能用于亲缘进程    

size: 共享内存区大小      

shmflg:  相当于open函数的权限位,也可以用8进制表示法                

shmflg如包含IPC_CREAT,表明如果指定的共享内存不存在,则新建一个对象     

 返回值: 正确返回 共享内存段标识符(共享内存id)

错误 -1 

void *shmat(int shmid, const void *shmaddr, int shmflg);

共享内存映射.    

shmid:要映射的共享内存区标识符

shmaddr: 将共享内存映射到指定地址(若为NULL,则表示由系统自动完成映射)      

shmflg: SHM_RDONLY:共享内存只读 默认0:共享内存可读可写      

成功:映射后的地址

错误 -1 

int shmdt(const void *shmaddr);

取消映射    

shmaddr: 共享内存映射后的地址      

成功 0

错误 -1 

int shmctl(int shmid, int cmd, struct shmid_ds *buf);    

共享内存控制函数.      

shmid:要操作的共享内存标识符      

cmd :   IPC_STAT  (获取对象属性)    IPC_SET (设置对象属性)          IPC_RMID (删除对象)
buf : 指定IPC_STAT/IPC_SET时用以保存/设置属性 当参数二为 IPC_RMID时 为 NULL      

成功 0 错误 -1 

代码:

    #include <stdio.h>
    #include <string.h>
    #include <stdlib.h>
    #include <strings.h>
    #include <sys/types.h>
    #include <sys/shm.h>

int main(int argc, char *argv[])
{
	/*ftok获取key,可作为生成shmid的键值,
	该key值由1.txt的节点号与'b'值组成*/
	key_t key = ftok("1.txt", 'b'); 
	if(key == -1)
	{
		perror("ftok");
		return -1;
	}
printf("key=%#x\n", key);
/*IPC_CREAT没有则创建共享内存,0777内存访问权限*/
int shmid = shmget(key, 1024, 0777 | IPC_CREAT);
if(shmid == -1)
{
	perror("shmget");
	return-1;
}

/*该指令可以查看共享内存是否创建成功,
显示出来的是内核共享内存的信息,不是映射区域*/
system("ipcs -m");

char *p = NULL;
/*参数NULL表示用户空间的映射地址随机分配
返回值为分配成功的空间首地址,也就是说p指针就是指向那个首地址
使用p就是使用共享内存*/
if((p = shmat(shmid, NULL, 0)) == (void*)-1)
{
	perror("shmat");
	return -1;
}

/*本次获取共享内存信息与上一次一样没有任何变化.*/
system("ipcs -m");

if(-1 == shmdt(p))
{
	perror("shmdt");
	return -1;
}

/*本次获取共享内存信息与上一次一样没有任何变化.
但是内存映射已经被取消,所以p不能再当共享内存使用
ipcs -m 查看的信息没有变化,因为内核的共享内存还在*/
system("ipcs -m");
if(-1 == shmctl(shmid, IPC_RMID, NULL))
{
	perror("shmctl");
	return -1;
}

/*内核共享内存消失,因为已经彻底被删除
注意:如果没有取消映射直接删除,本次内存信息查看会发现并没有删除成功
因为映射内存还在,所以内核不敢直接删除内核共享,但是程序运行完后ipcs -m将
不会看到内核共享内存了,因为程序结束后用户空间都将释放包括用户共享内存

所以今天看到的程序没有关闭共享内存还在,结束就不在的原因是因为没有取消映射*/
system("ipcs -m");

return 0;
}

2.消息队列

注意:遇到多个同种类型的消息,按照队列先进先出的方式进行输出

1、打开或者创建消息队列

int msgget(key_t key, int flag);

头文件: #include <sys/types.h>  #include <sys/ipc.h>  #include <sys/msg.h>

参数: key:IPC_PRIVATE(只能用于亲缘进程) 或者是 ftok函数返回值

flag:消息队列访问权限(IPC_CREAT|0644)

返回值: 成功:消息队列ID号 失败:-1

2、发送消息(添加消息)

int msgsnd(int msgid, const void *msgbuf, size_t size, int flag);

参数:

msgid:消息队列ID号

msgbuf:消息的缓冲区首地址(指向消息的指针)

消息类型必须如下所示:

struct msgbuf{

        long type; //消息类型         

        char buf[32]; //消息正文

};

size: 消息正文的长度

flag: 0:阻塞等待发送消息完成 IPC_NOWAIT:非阻塞

返回值: 成功:0 失败:-1

3、接收消息(读取消息)

int msgrcvd(int msgid, void *msgbuf, size_t size, long msgtype, int flag);

参数:

        msgid: 消息队列ID号

        msgbuf: 存储消息的缓冲区

        size: 消息正文的长度

        msgtype: 要读取的消息的类型

        flag: 0:若无数据,一直阻塞下去 IPC_NOWAIT:非阻塞

返回值:

成功:实际读取的字节数

失败:-1

4、消息队列的控制

int msgctl(int msgid, int cmd, struct msqid_ds *buf);

参数:

msqid:消息队列的 ID号

cmd: IPC_STAT:读取消息队列的属性,并将其保存在buf指向的缓冲区中。

IPC_SET:设置消息队列的属性。这个值取自buf参数。

IPC_RMID:从系统中删除消息队列。

buf:消息队列缓冲区

返回值: 成功:0 失败:-1

代码案例如下,用消息队列来完成子进程输入,父进程输出

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/msg.h>
struct msgbuf{
    long mtype;
    char buf[32];
    int data;
};//消息结构类型定义
int main(int argc, char *argv[])
{ 
    
    int msgid=msgget(IPC_PRIVATE,IPC_CREAT| 0644);//创建消息队列
    if(msgid<0)
    {
        perror("msgget");
        return -1;
    }
    system("ipcs -q");
    pid_t pid=fork();//创建子进程
    if(pid<0)
    {
        perror("fork");
        return -1;
    }
    else if(0==pid)
    {
        while(1)
        {
             struct msgbuf msg;//定义结构体变量,用来存储消息和消息的类型
             msg.mtype=10;//指定该条消息的 类型
             msg.data=1024;//一个消息正文赋值
             fgets(msg.buf,32,stdin);//从标准输入读取一行数据到 buf消息正文中
             if(msgsnd(msgid,&msg,sizeof(msg)-sizeof(long),0)<0)
             将消息发送(添加)到消息队列,添加完成该函数才能返回
             {
                perror("msgsnd");
                return -1;
             }
             if(strncmp(msg.buf,"quit",4)==0)//检测到quit退出循环
             {
                     break;
             }
        }
    }
    else
    {
        waitpid(pid,NULL,WNOHANG);//等待回收子进程资源
        while(1)
        {
             struct msgbuf msg={0};
             //接收消息
             int ret=msgrcv(msgid,&msg,sizeof(msg)-sizeof(long),10,0);
             if(ret<0)
             {
                 perror("msgrcv");
                 return -1;
             }
             printf("ret:%d\n",ret);
             printf("data:%d\n",msg.data);
             printf("recv:%s\n",msg.buf);
             if(strncmp(msg.buf,"quit",4)==0)
             {
                    break;
             }
        }
        //删除消息队列
       if(msgctl(msgid,IPC_RMID,NULL)<0)
       {
            perror("msgctl");
            return -1;
       }
       system("ipcs -q");//查看消息队列
    }
    return 0;
} 

3.信号灯集

概念:

由一个或多个信号灯组成,每个信号灯都是一个计数信号灯,由内核维护。

信号灯集的创建

函数原型:int semget(key_t key,int nsems,int semflag)

头文件:

#include <sys/types.h>

#include <sys/ipc.h>

#include <sys/sem.h>

参数:

key:可以是IPC_PRIVATE,也可以是ftok返回值

nsems:创建灯集中信号灯的个数

semflag:IPC_CREAT| 0640(访问权限)

返回值:

成功:信号灯集的id号

失败:-1

信号灯的操作函数

函数原型:semop(int semid,struct sembuf* sem,size_t nsops)

参数:

semid:信号灯集的id号

sem:结构体指针

        struct sembuf{

                short sem_num;//待操作的信号灯的编号

                short sem_op;//信号灯的操作方式(0:等待 -1:P操作,申请资源,1:V操作,释放资源)

                short sem_flg;//0:阻塞等待  IPC_NOWAIT:非阻塞

                };

nsops:要操作的信号灯个数,一般为1

返回值:

        成功:0;

        失败:-1

信号灯集的控制函数

函数原型:int semctl(int semid,int semnum,int cmd,union semun arg)

参数:(参数有三个或四个,由cmd这个参数来决定)

semid:信号灯集的id号

semnum:要修改的信号灯的编号

cmd:

        SETVAL:设置信号灯的值

        GETVAL:获取信号灯的值

        IPC_RMID:删除信号灯

返回值:

        成功:0

        失败:-1

注意:

当cmd使用SETVAL,GETVAL时必须要用第四个参数

联合体类型变量

union semun{

        int val;//信号灯的值

        struct semid_ds* buf;

        unsigned short* array;

        struct seminfo* _buf;

};

案例:用信号灯集操作来完成子进程输入,父进程输出

#include <unistd.h>
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <string.h>
#include <sys/sem.h>
union semun{
    int val;
    struct semid_ds* buf;
    unsigned short* array;
    struct seminfo* _buf;
};
int main(int argc, char *argv[])
{ 
    //创建信号灯集
    int semid=semget(IPC_PRIVATE,2,IPC_CREAT| 0640);
    if(semid<0)//判断错误
    {
        perror("semget");
        return -1;
    }
    system("ipcs -s");//查看信号灯集
    union semun value1={0};//初始化信号灯0
    semctl(semid,0,SETVAL,value1);//设置信号灯0初始值为0
    union semun value2={1};//初始化信号灯1
    semctl(semid,1,SETVAL,value2);//设置信号灯1初始值为1
    pid_t pid;
    int shmid;
    char* shm_add;
    //创建共享内存
    shmid=shmget(IPC_PRIVATE,32,0644);
    if(shmid<0)//判断错误
    {
        perror("shmget");
        exit(1);
    }
    else
    {     
         printf("Create share-memory:%d\n",shmid);//打印成功信息
    }
    system("ipcs -m");//查看共享内存
    if((shm_add=shmat(shmid,NULL,0))==(void*)-1)//判断映射是否成功
    {
        perror("shmat");
        return -1;
    }
    pid=fork();//创建子进程
    if(-1==pid)//判断子进程创建是否成功
    {
        perror("fork");
        exit(1);
    }
    else if(0==pid)//子进程
    {
        while(1)
        {
            struct sembuf buf1={.sem_num=1,.sem_op=-1,.sem_flg=0};
            //NUM为1,OP为-1,对信号灯1进行P操作
            semop(semid,&buf1,1);
            fgets(shm_add,32,stdin);
            struct sembuf buf2={0,1,0};
            //NUM为0,OP为1,对信号灯0进行V操作
            semop(semid,&buf2,1);
            if(strstr(shm_add,"quit")!=NULL)
            {
                break;
            }
        }
    }
    else//父进程
    {       
        waitpid(pid,NULL,WNOHANG);//回收子进程资源
        while(1)
        {
            struct sembuf buf1={0,-1,0};
            //NUM为0,OP为-1,对信号灯0进行P操作
            semop(semid,&buf1,1);
            printf("recv:%s\n",shm_add);
            struct sembuf buf2={1,1,0};
            //NUM为1,OP为1,对信号灯1进行V操作
            semop(semid,&buf2,1);
            if(strncmp(shm_add,"quit",4)==0)
            {
                break;
            }
        }
    }
    if(-1==shmdt(shm_add))
    {
        perror("shmdt");
        return -1;
    }
    char buff[32]={0};
    sprintf(buff,"ipcrm -m %d -s %d",shmid,semid);
    system(buff);
    system("ipcs -m -s");
} 

最后我们用一张图来总结一下进程间通信方式的特点:

 

  • 5
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值