linux进程间通信(上)

一、进程的基本概念

进程是指一个具有独立功能的程序在某个数据集合上的一次动态执行过程,他是操作系统进行资源分配的最小单元(线程是操作系统进行调度的最小单元)。进程具有并发性、动态性、交互性和独立性等主要特性

  • 并发性:指的是系统中可以有多个进程同时并发执行,相互之间不受干扰
  • 动态性:指的是进程都有完整的生命周期,而且在进程的生命周期内,进程的状态是不断变化的,另外进程具有动态的地址空间(包括代码、数据和进程控制块)
  • 交互性:指的是进程在执行过程中可能会与其他进程发生直接或者间接的通信,如进程同步和进程互斥等,需要为此添加一定的进程处理机制
  • 独立性:指的是进程是一个完整的资源分配和调度的基本单位,各个进程的地址空间是相互独立的,需要为此添加一定的进程处理机制

从操作系统的角度看,进程是程序执行时相关资源的总成。当进程结束时,所有资源被操作系统回收

二、进程间通信

由于在操作系统中进程具有独立性,所以为了实现进程之间通信,linux操作系统中提供了多种通信方式

  • 无名管道(pipe)及有名管道(fifo):无名管道可用于具有亲缘关系进程间的通信;有名管道除具有无名管道的功能外,他还允许无亲缘关系进程间的通信
  • 信号(signal):信号时在软件层次上对中断机制的一种模拟,他是比较复杂的通信方式,用于通知进程某事件的发生。一个进程收到一个信号与处理器收到一个中断请求处理的过程类似
  • 消息队列(message queue):消息队列时消息的链接表。他克服了前两种通信方式的信息量有限的缺点,具有写权限的进程可以按照一定的规则向消息队列中添加新消息;对消息队列有读权限的进程则可以从消息队列中读取消息
  • 共享内存(shared memory):可以说这是最有效的进程间通信方式。他是的多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据的更新。这种通信方式要依靠某种同步机制,如互斥锁和信号量等
  • 信号量(semaphore)主要作为进程之间以及同一进程之间的不同线程之间的同步和互斥手段
  • 套接字(socket):这是一种使用广泛的进程间通信机制,它可用于网络中不同主机之间的进程间通信,应用非常广泛

接下来对各种通信方式进行说明,并用实例进行演示

无名管道与有名管道

1、 无名管道

无名管道有如下特点:

  • 只能用于具有亲缘关系的进程之间的通信(父子进程或者兄弟进程)
  • 是一种单工的通信方式,具有固定的读端与写端
  • 管道也可以看作是一种特殊文件,对于它的读写也可以使用普通的read()与write()等函数,但是他不属于任何文件系统,并且只存在与内存中

(1)无名管道的创建函数

所需头文件 #include <unistd.h>
函数原型 int pipe(int pipefd[2]);
函数传入值 fd:包含两个元素的整型数组,存放管道对应的文件描述符
函数返回值 成功:返回0
失败:返回-1

当一个管道建立时,它会创建两个文件描述符:fd[0]、fd[1]。其中fd[0]固定用于读管道,而fd[1]固定用于写管道。

(2)管道读写说明

用pipe()函数创建的管道两端处于一个进程中。由于管道主要时用于不同进程间的通信,通常先是创建一个管道,再调用fork()函数创建一个子进程,该子进程会继承父进程所创建的管道。需要注意的是,无名管道是单工的通信方式,即进程要么只能读管道要么只能写管道。父子进程虽然都拥有管道的读端和写端,但是只能使用其中一个,这样就应该把不使用的读端或写端文件描述符关闭。例如将父进程的写端fd[1]和子进程的读端fd[0]关闭,此时父子进程之间就建立了一条“子进程写入父进程读取”的通道。同样反过来也可以。

(3)实例演示:无名管道实现父进程写子进程读

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


#define  MAX_DATEA_LEN 100

int main(int argc, char *argv[])
{
	pid_t pid;
	int pipe_fd[2];
	const char date[] = "Pipe test programe";
	char buf[MAX_DATEA_LEN] = {0};
	int real_read,real_write;
	
	if(pipe(pipe_fd) == -1)
	{
		perror("fail to pipe");
		exit(-1);
	}
	
	if((pid = fork()) == -1)
	{
		perror("fail to fork");
		exit(-1);
	}
	else if(pid == 0)
	{
		/*子进程关闭写文件描述符,并通过1s等待父进程已关闭相应的读文件描述符*/
		close (pipe_fd[1]);
		sleep(1);
		if(real_read = read(pipe_fd[0], buf, MAX_DATEA_LEN));
		{
			printf("% date bytes read from the pipe is '%s'\n", real_read, buf);
		}		
		close(pipe_fd[0]);
		exit (0);
	}
	else if(pid > 0)
	{
		/*关闭读文件描述符*/
		close (pipe_fd[0]);
		if((real_write = write(pipe_fd[1], date, strlen(date))) != -1)
		{
			printf("parent wrote %d byte : '%s'\n", real_write, date);
		}
		close(pipe_fd[1]);
		waitpid(pid, NULL, 0);
		exit (0);
	}	
}

运行结果如下:

2、有名管道

有名管道有如下特点:

  • 它可以使互不相关的两个进程实现彼此通信
  • 该管道可以通过路径来指出,并且在文件系统中是可见的。在建立了管道之后,两个进程就可以把它当作普通文件一样进行读写操作,使用非常方便
  • FIFO严格的遵循先进先出规则,对管道及FIFO的读总是从开始处返回数据,对他们的写则把数据添加到末尾。有名管道不支持lseek()等文件定位操作

有名管道的创建使用mkfifo()函数,该函数类似于文件中的open()操作,可以指定管道的路径与访问权限(用户也可以在命令行使用“mknod <管道名>”来创建有名管道)。在创建成功之后就可以使用open()、read()、write()这些函数了。与普通文件一样,对于为读二打开的管道可在open()中设置O_RDONLY,对于为写而打开的管道可在open()中设置O_WEONLY。

缺省情况下,如果当前fifo中没有数据,读进程将一直阻塞到有数据写入或者是fifo写端都关闭

(1)有名管道的创建函数mkfio()

所需头文件 #include <sys/types.h>
#include <sys/stat.h>
函数原型 int mkfifo(const char *pathname, mode_t mode);
函数传入值 pathname:要创建的管道名
mode;管道的访问权限
函数返回值 成功:返回0
失败:返回-1

 (2)实例演示:该演示实例包括两个程序,一个用于读管道,另一个用于写管道。其中在读管道的程序里创建管道,并且作为main函数里的参数由用户输入要写入的内容。读管道的程序会读出用户写入管道的内容

/*
fifo_write.c
*/
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>

int main(int argc, char *argv[])
{
	int fd;
	int num_write;
	if(argc < 2)
	{
		printf("Usage: ./fifo_write string\n");
		exit(-1);
	}
	
	if((fd = open("MYFIFO", O_WRONLY)) < 0)
	{
		perror("fail to open fifo");
		exit(-1);
	}
	
	if((num_write = write(fd, argv[1], strlen(argv[1])+1)) > 0)
	{
		printf("Write '%s' to FIFO\n", argv[1]);
	}
	
	close(fd);
	return 0;
}
/*
fifo_write.c
*/
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdlib.h>

int main(int argc, char *argv[])
{
	int fd;
	char buf[256];
	int num_read = 0;
	
	/*判断有名管道是否存在,若没有创建,则以相应的权限创建*/
	if(access("MYFIFO", F_OK) == -1)
	{
		if(mkfifo("MYFIFO", 0666) == -1)
		{
			perror("fail to mkfifo");
			exit(-1);
		}
	}
	/*以只读的方式打开有名管道*/
	if((fd = open("MYFIFO", O_RDONLY)) == -1)
	{
		perror("fail to open");
		exit(-1);
	}
	
	while(1)
	{
		memset(buf, 0, sizeof(buf));
		if((num_read = read(fd, buf, sizeof(buf))) > 0)
		{
			printf("Read '%s' from FIFO\n", buf);
		}
	}
	close(fd);
	return 0;
}

运行结果如下: 

3、信号   

(1)信号概述:

信号是在软件层次上对中断机制的一种模拟。信号是异步的:一个进程不必通过任何操作来等待信号的到达。事实上,进程也不知道信号到底什么时候到达。信号可以直接进行用户空间进程和内核进程之间的交互,内核进程也可以利用它来通知用户空间进程发生了哪些系统事件。他可以在任何时候发给某一-进程,而无需知道该进程的状态,如果当前并未处于执行状态,则该信号就由内核保存起来,直到该进程恢复再传递给它为止;如果一个信号被进程设置为阻塞,则该信号的传递被延迟,直到其阻塞被取消时才被传递给进程。

信号是进程间通信机制中唯一的异步通信机制。我们可以把这种机制看作异步通知,通知接收信号的进程有哪些事情发生了,他除了基本的通知功能外,还可以传递附加消息。  信号的产生有硬件来源和软件来源,常用的信号相关的函数会有kill()、raise()、alarm()、setitimer()和sigqueue()等,进程可以通过三种方式来相应信号,忽略信号、捕捉信号、执行默认操作

发送信号的函数:kill()、raise()

设置信号函数:signal()、sigaction()

其他函数:alarm()、pause()

(2)信号的发送

kil()和raise()函数是信号发送函数,kill()函数可以向指定的进程发送信号,但是raise()函数只允许向自身发送信号

kill()函数语法要点:

所需头文件 #include <sys/types.h>
 #include <signal.h>
函数原型 int kill(pid_t pid, int sig);
函数传入值 pid 正数:发送信号给进成为pid的进程
0:信号被发送到所有和当前进程在同一进程组的进程
-1:信号发送给所有的进程表中的进程(除了进程号最大的进程外)
< -1:信号发送给进程组号为-pid的每一个进程
函数返回值 成功:0
错误:-1

raise()函数语法要点:

所需头文件 #include <sys/types.h>
#include <signal.h>
函数原型 int raise(int sig);
函数传入值 sig:信号类型
函数返回值 成功:0
错误:-1

(3)实例演示:

下面实例使用fork()函数创建了一个子进程,在子进程中使用raise()函数向自身发送SIGSTOP信号,使子进程暂停;接下来再在父进程中调用kill()函数向子进程发送信号,使用的是SIGKILL。

/*
kill_raise.c
*/

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


int main(int argc, char *argv[])
{
	pid_t pid;
	
	if((pid = fork()) < 0)
	{
		perror("fork error");
		exit(-1);
	}
	
	if(pid == 0)
	{
		/*在子进程中使用raise()函数发出SIGSTOP信号,使子进程暂停。raise()函数只允许向自身发送信号*/
		printf("child(pid:%d) is waiting for any signal\n", getpid());
		raise(SIGSTOP);
	}
	else
	{
		/*在父进程中收集子进程的状态,并调用kill*()函数发送信号*/
		sleep(1);
		if((waitpid(pid, NULL, WNOHANG)) == 0)
		{
			kill(pid, SIGKILL);
			printf("parent kill child process %d\n", pid);
		}
		waitpid(pid, NULL, 0);
		exit(0);
	}	
}

运行结果如下:

 

(4)定时器信号:alarm()、pause()

alarm()也成为闹钟函数,他可以在进程中设置一个定时器。当定时器指定的时间到时,它就向进程发送SIGALARM信号。要注意的是,一个进程只有一个闹钟时间,如果在调用alarm()函数之前设置过闹钟时间,则任何以前的闹钟时间都被新值所代替。pause()函数是用于将调用进程挂起直至接收到信号为止。

alarm()函数语法要点

所需头文件 #include <unistd.h>
函数原型 unsigned int alarm(unsigned int seconds);
函数传入值 seconds:指定秒数,系统经过seconds秒之后向进程发送SIGALARM信号
函数返回值 成功:如果调用此alarm()前,进程中已经设置了闹钟时间,则返回上一个闹钟剩余的时间。如果是第一次设置则返回0
出错:-1

pasue()函数的语法要点

所需头文件 #include <unistd.h>
函数原型 int pause(void);
函数传入值
函数返回值 -1,并且把errno值设置为EINT
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
    /*调用定时器alarm定时器函数*/
    alarm(5);
    pause();
    printf("I have been wake up \n");    //这条语句不会被执行
}

(5)信号的设置:signal()和sigaction()

有关signal()函数前面已经介绍过

参考:https://blog.csdn.net/David_361/article/details/86569985

这里主要对sigaction()函数做一下补充

sigaction()函数相对signal()函数更加健壮、功能更全。sigaction可以一次得到设置新捕获函数和获取旧的捕获函数(其实还可以单独设置新的捕获或者单独只获取旧的捕获函数),而signal函数不能单独获取旧的捕获函数而必须在设置新的捕获函数的同时才获取旧的捕获函数。

所需头文件 #include <signal.h>
函数原型 int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
函数传入值 signum:信号类型,除SIGKILL及SIGSTOP之外的任何一个信号
act:指向sigaction结构体的指针,包含对指定信号的处理
oldact:保存信号原先的处理方式
函数返回值 成功:0
错误:-1

 下面看一下struct sigsction结构体

struct sigaction 
{
   void     (*sa_handler)(int);
   void     (*sa_sigaction)(int, siginfo_t *, void *);
   sigset_t   sa_mask;
   int        sa_flags;
   void     (*sa_restorer)(void);
};
  •  sa_handler是一个函数指针,指向函数处理函数。它既可以是用户自定义的处理函数,也可以是SIG_DFL(采用默认的处理方式)或SIG_IGN(忽略信号)。信号处理函数只有一个参数,即信号类型
  • sa_mask是一个信号集合,用来指定在信号处理函数执行过程中那些信号被屏蔽
  • sa_flags中包含了许多标志位,都是和信号处理相关的选项

实例演示 ,分别用signal()和sigaction()函数实现捕捉信号,并进行给定的处理

/*
signal.c
*/
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>

void my_func(int sign_no)
{
	if(sign_no == SIGINT)
	{
		printf("I have got SIGINT\n");
	}
	else if(sign_no == SIGQUIT)
	{
		printf("I have got SIGQUIT\n");
	}
	
}

int main(int argc, char *argv[])
{
	printf("waiting for signal ...\n");
	/*设置信号处理函数*/
	signal(SIGINT, my_func);
	signal(SIGQUIT,my_func);
	pause();
	exit(0);
}
/*
sigaction.c
*/
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>

void my_func(int sign_no)
{
	if(sign_no == SIGINT)
	{
		printf("I have got SIGINT\n");
	}
	else if(sign_no == SIGQUIT)
	{
		printf("I have got SIGQUIT\n");
	}
	
}

int main(int argc, char *argv[])
{
	struct sigaction action;
	/*sigaction结构体的初始化*/
	sigaction(SIGINT, 0, &action);
	action.sa_handler = my_func;
	sigaction(SIGINT, &action, 0);
	
	sigaction(SIGQUIT, 0, &action);
	action.sa_handler = my_func;
	sigaction(SIGQUIT, &action, 0);
	printf("waiting for signal ...\n");
	
	pause();
	exit(0);
}

实验结果如图:

4、信号量

5、共享内存

6、消息队列

进程间通信下:https://blog.csdn.net/David_361/article/details/86605455

猜你喜欢

转载自blog.csdn.net/David_361/article/details/86572307