Linux 系统应用编程——进程间通信(上)

       现在再Linux应用较多的进程间通信方式主要有以下几种:

1)无名管道(pipe)及有名管道(fifo):无名管道可用于具有亲缘关系进程间的通信;有名管道除具有管道相似的功能外,它还允许无亲缘关系进程使用;

2)信号(signal):信号是在软件层次上对中断机制的一种模拟,它是比较复杂的通信方式,用于通知进程某事件发生。一个进程收到一个信号与处理器收到一个中断请求处理的过程类似;

3)消息队列(message queue):消息队列是消息的链接表,包括POSIX消息队列和System V 消息队列。它克服了前两种通信方式中信息量有限的缺点。具有写权限的进程可以按照一定的规则向消息队列中添加新消息;对消息队列有读权限的进程则可以从消息队列中读取消息。

4)共享内存(shared memory):可以说这时最有效的进程间通信方式。它使得多个进程可以访问同一块内存空间,不同进程可以及时查看对方进程中对共享数据的更新。这种通信方式需要依靠某种同步机制,如互斥锁和信号量等。

5)信号量(semaphore):主要作为进程之间以及统一进程的不同线程之间的同步和互斥手段。

6)套接字(socket):这时一种使用更广泛的进程间通信机制,它可用于网络中不同主机之间的进程间通信,应用非常广泛。


管道通信

        管道是Linux 中进程间通信的一种方式,它把一个程序的输出直接连接到另一个程序的输入,Linux 的管道主要包括两种:无名管道和有名管道。

一、无名管道

      无名管道是Linux中管道通信的一种原始方法,他有如下特点:

1)只能用于具有亲缘关系的进程之间的通信(也就是父子进程或兄弟进程之间);

2)是一个单工的通信模式,具有固定的读端和写端;

3)管道也可以看成一种特殊的文件,对于它的读写也可是使用普通的read() 、write()等函数,但是它不属于任何文件系统,并且只存在于内存中;(其字节大小为0)


1、无名管道的创建与关闭

       无名管道是基于文件描述符的通信方式。当一个管道创建时,它会创建两个文件描述符:fd[0] 、fd[1] 。其中 fd[0] 固定用于读管道,而 fd[1] 固定用于写管道,如下图,这样就构成了一个单向的数据通道:

Linux 系统应用编程——进程间通信(上)_第1张图片

管道关闭时只需要用 close() 函数将这两个文件描述符关闭即可。


2、管道创建函数

      创建管道可以通过 pipe() 来实现,其语法如下:

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

3、管道读写说明

       用pipe() 函数创建的管道两端处于一个进程中。由于管道主要是用于不同进程间的通信,通常是先创建一个管道,再调用 fork () 函数创建一个子进程,该子进程会继承父进程所创建的管道。

         需要注意的是,无名管道是单工的工作方式,即进程要么只能读管道,要么只能写管道。父子进程虽然都拥有管道的读端和写端,但是只能使用其中一个(例如,可以约定父进程读管道,而子进程写管道)。这样就应该把不使用的读端或写端文件描述符关闭。

Linux 系统应用编程——进程间通信(上)_第2张图片

例如:如果将父进程的写端 fd[1] 和子进程的读端 fd[0] 关闭。此时,父子进程之间就建立了一条“子进程写入 父进程读取”的通道。同样,也可以关闭父进程的 fd[0] 和子进程的fd[1] ,这样就可以建立一条“父进程写入子进程读取”的通道。另外,父进程也可以创建 多个子进程,各个子进程都继承了管道的fd[0] 和 fd[1] ,这样就建立子进程之间的数据通道。


4、管道读写注意:

1)只有管道的读端存在时,向管道写入数据才有意义,否则,向管道中写入数据的进程将收到内核传来的 SIGPIPE 信号 (通常为Broken Pipea错误)。

2)向管道写入数据时,Linux 将不保证写入的原子性 , 管道缓冲区只要有空间,写进程就会试图向管道写入数据。如果管道缓冲区已满,那么写操作将一直阻塞。

3)父进程在运行时,它们的先后次序必不能保证。为了确保父子进程已经关闭了相应的文件描述符,可在两个进程中调用 sleep() 函数,当然,用互斥和同步会更好;


下面是一个实例:

#include 
#include 
#include 
#include 

int pid,pid1,pid2;

int main(int argc, const char *argv[])
{
	int fd[2];
	char outpipe[100],inpipe[100];

	if(pipe(fd) < 0)
	{
		perror("pipe error!");
		return -1;
	}

	if((pid1 = fork()) < 0)
	{
		perror("fork pid1 error");
		return -1;
	}
	else if(pid1 == 0)
	{
		printf("Child1's pid is %d\n",getpid());
		close(fd[0]);
		strcpy(outpipe,"Child 1 is sending a message!");
		if(write(fd[1],outpipe,50) == -1)
		{
			perror("Child 1 write to outpipe error");
			return -1;
		}
		exit(0);
	}
		
	if((pid2 = fork()) < 0)
	{
		perror("fork pid2 error");
		return -1;
	}
	else if(pid2 == 0)
	{
		printf("Child2's pid is %d\n",getpid());
		close(fd[0]);
		strcpy(outpipe,"Child 2 is sending a message!");
		sleep(1);
		if(write(fd[1],outpipe,50) == -1)
		{
			perror("Child 2 write to outpipe error");
			return -1;
		}
		exit(0);
	}
	
	close(fd[1]);
	pid = wait(NULL);
	printf("%d process is over!\n",pid);
	if(read(fd[0],inpipe,50) == -1)
	{
		perror("read Child1 pipe error");
		return -1;
	}
	printf("%s\n",inpipe);

	pid = wait(NULL); //回收第二个结束的子进程
	printf("%d process is over!\n",pid);
	if(read(fd[0],inpipe,50) == -1)
	{
		perror("read Child1 pipe error");
		return -1;
	}
	printf("%s\n",inpipe);

	return 0;
}

执行结果如下:

fs@ubuntu:~/qiang/pipe$ ./pipe
Child2's pid is 8504
Child1's pid is 8503
8503 process is over!
Child 1 is sending a message!
8504 process is over!
Child 2 is sending a message!
fs@ubuntu:~/qiang/pipe$ 


二、有名管道

         有名管道(FIFO)是对无名管道的一种改进,它具有如下特点:

1)它可以使互不相关的两个进程实现彼此通信;

2)该管道可以通过路径名来指出,并且在文件系统中是可见的。在建立了管道之后,两个进程就可以把它当做普通文件一样进行读写操作,使用非常方便;

3)FIFO严格地遵循先进先出规则,对管道及 FIFO 的读总是从开始处返回数据,对它们的写则把数据添加到末尾。有名管道不支持如lseek()等文件定位操作;

        有名管道(FIFO)的创建可以使用 mkfifo() 函数,该函数类似文件中的open() 操作,可以指定管道的路径和访问权限 (用户也可以在命令行使用 “mknod <管道名>”来创建有名管道)。

        在创建管道成功以后,就可以使用open()、read() 和 write() 这些函数了。与普通文件一样,对于为读而打开的管道可在 open() 中设置 O_RDONLY,对于为写而打开的管道可在 open() 中设置O_WRONLY。

1、对于读进程

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

2、对于写进程

    只要FIFO有空间,数据就可以被写入。若空间不足,写进程会阻塞,知道数据都写入为止;

mkfifo() 函数语法如下:

所需头文件 #include
#include
函数原型  int mkfifo( const char *filename,mode_t mode)
参数 mode:管道的访问权限
函数返回值 成功:0
出粗:-1

下面是个实例,来学习有名管道的使用

create.c

#include 
#include 
#include 
#include 
#include 

int main(int argc,char *argv[])
{
	if(argc < 2)
	{
		printf("Usage:%s ",argv[0]);
		return -1;
	}

	if(mkfifo(argv[1],0664) < 0)
	{
		perror("mkfifo fails");
		exit(-1);
	}

	return 0;
}
write_fifo.c
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define BUFFER_SIZE 1024

int main(int argc, const char *argv[])
{
	int fd;

	if(argc < 2)
	{
		printf("Usage:%s ",argv[0]);
		return -1;
	}

	if((fd = open(argv[1],O_WRONLY)) < 0)
	{
		perror("open error");
		exit(-1);
	}

	printf("open fifo %s for writing success!\n",argv[0]);
	char buffer[BUFFER_SIZE];
	ssize_t n;

	while(fgets(buffer,BUFFER_SIZE,stdin))
	{
		if((n = write(fd,buffer,strlen(buffer))) == -1)
		{
			perror("write fails");
			break;
		}
	}
	return 0;
}
read_fifo.c
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define BUFFER_SIZE 1024

int main(int argc, const char *argv[])
{
	int fd;

	if(argc < 2)
	{
		printf("Usage:%s ",argv[0]);
		return -1;
	}

	if((fd = open(argv[1],O_RDONLY)) < 0)
	{
		perror("open error");
		exit(-1);
	}

	printf("open fifo %s for reading success!\n",argv[0]);
	char buffer[BUFFER_SIZE];
	ssize_t n;

	while(1)
	{
		if((n = read(fd,buffer,BUFFER_SIZE)) == -1)
		{
			perror("read fails");
			return -1;
		}
		else if(n == 0)
		{
			printf("peer close fifo\n");
			break;
		}
		else
		{
			buffer[n] = '\0';
			printf("read %d bytes from fifo:%s\n",n,buffer);
		}
	}
	return 0;
}

执行结果如下:

写端:

fs@ubuntu:~/qiang/fifo$ ./create_fifo tmp
fs@ubuntu:~/qiang/fifo$ ./write_fifo tmp
open fifo ./write_fifo for writing success!
xiao
zhi
qiang
^C
fs@ubuntu:~/qiang/fifo$ 

读端:

fs@ubuntu:~/qiang/fifo$ ./read_fifo tmp
open fifo ./read_fifo for reading success!
read 5 bytes from fifo:xiao

read 4 bytes from fifo:zhi

read 6 bytes from fifo:qiang

peer close fifo
fs@ubuntu:~/qiang/fifo$ 

这里执行时,可以看到,单独打开读或写,二者会一直阻塞,直到都打开,才会打印第一句话,当写端关闭时,读端也会停止。


三、信号通信

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

Linux 系统应用编程——进程间通信(上)_第3张图片

1、信号的生存周期:

Linux 系统应用编程——进程间通信(上)_第4张图片


2、进程可以通过3种方式来响应一个信号

1)忽略信号

即对信号不做任何处理,其中,有两个信号不能忽略:SIGKILL及 SIGSTOP;

2)捕捉信号

定义信号处理函数,当信号发生时,执行相应的处理函数。

3)执行默认操作

Linux 对每种信号都规定了默认操作;(后面会给出信号列表)

这里介绍几个常用的信号

信号名 含义 默认操作
SIGINT 该信号在用户输入INTR字符(通常是Ctrl + C)时发出,
终端驱动程序发送该信号并送到前台进程中的每一个进程
终止进程
SIGQUIT 该信号和SIGINT类似,但由QUIT字符(通常是Ctrl + \)来
控制
终止进程
SIGKILL 该信号用来立即结束程序的运行;
不能被阻塞、处理和忽略;
终止进程
SIGALARM 该信号当一个定时器到时的时候发出; 终止进程
SIGSTOP 该信号用于暂停一个进程;
不能被阻塞、处理和忽略;
暂停进程
SIGTSTP 该信号用于交互停止进程(挂起),由Ctrl + Z 来发出 终止进程

3、信号处理流程

Linux 系统应用编程——进程间通信(上)_第5张图片

下面是内核如何实现信号机制,即内核如何向一个进程发送信号、进程如何接收一个信号、进程怎样控制自己对信号的反应、内核在什么实际处理和怎样处理进程收到的信号。

内核对信号的基本处理方法

       内核给一个进程发送软中断信号的方法是,进程所在的进程表项的信号域设置对于该信号的位(内核通过在进程的 struct task_struct 结构中的信号域中设置相应的位来实现向一个进程发送信号)。这里要补充的是,如果信号发送给一个正在睡眠的进程,那么要看该进程进入睡眠的优先级,如果进程睡眠在可被中断的优先级上,则唤醒进程;否则仅设置进程表中信号域相应的位,而不唤醒进程。这一点比较重要,因为进程检查是否收到信号的时机是一个进程在即将从内核态返回到用户态时;或者,在一个进程要进入或离开一个适当的低调度优先级睡眠状态时。

       内核处理一个进程收到的信号的时机是一个进程从内核态返回用户态时。所以,当一个进程在内核态运行时,软中断信号并不立即起作用要等到将返回用户态时才处理。进程只有处理完信号才会返回用户态,进程在用户态下不会有未处理完的信号。

       内核处理一个进程收到的软中断信号是在该进程的上下文中,因此,进程必须处于运行状态。处理信号有三种类型:进程接收到信号后退出;进程忽略该信号;进程收到信号后执行用户自定义的使用系统调用signal() 注册的函数。当进程接收到一个它忽略的信号时,进程丢弃该信号,就像从来没有收到该信号似得,而继续运行。如果进程收到一个要捕捉的信号,那么进程从内核态返回用户态时执行用户定义的函数。而且执行用户定义的函数的方法很巧妙,内核是在用户栈上创建一个新的层,该层中将返回地址的值设置成用户定义的处理函数的地址,这样进程从内核返回弹出栈顶时就返回到用户定义的处理函数处,从函数返回再弹出栈顶时,才返回原来进入内核的地方。这样做的原因是用户定义的处理函数不能且不允许在内核态下执行(如果用户定义的函数在内核态下运行的话,用户就可以获得任何权限)。

       在信号的处理方法中有几点特别要引起注意:

1)在一些系统中,当一个进程处理完中断信号返回用户态之前,内核清除用户区中设定的对该信号的处理例程的地址,即下一次进程对该信号的处理方法又改为默认值,除非在下一次信号到来之前再次调用 signal() 系统调用。这可能会使得进程在调用 signal() 之前又得到该信号而导致退出。在BSD系统中,内核不再清除该地址。但不清楚该地址可能使得进程因为过多过快的得到某个信号而导致堆栈溢出。为了避免出现上述情况。在BSD中,内核模拟了对硬件中断的处理方法,即在处理某个中断时,阻止接收新的该类中断。


4、信号相关函数

1)信号发送:kill() 和 raise()

       kill() 函数同读者熟知的kill 系统命令一样,可以发送信号给进程或进程组(实际上,kill 系统命令就是由 kill () 函数实现的)。需要注意的是,它不仅可以终止进程,也可以向进程发送其他信号;

        与kill() 函数不同的是,raise() 函数只允许进程向自身发送信号;

kill() 函数语法

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

sig :信号类型
函数返回值 成功 :0
出错: -1

raise() 函数的语法

所需头文件 #include
#include
函数原型 int raise(int sig);
函数传入值 sig :信号类型
函数返回值 成功:0
出错: -1

这里 raise() 等价于 kill ( getpid() , sig) ;

下面举一个实例:

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

int main(int argc, char *argv[])
{
	pid_t pid;
	int ret;

	if((pid = fork()) < 0)
	{
		perror("fork error");
		exit(-1);
	}
	
	if(pid == 0)
	{
		printf("child(pid : %d)is waiting for any signal\n",getpid());
		raise(SIGSTOP);
		exit(0);
	}
	
	sleep(1);
	if((waitpid(pid,NULL,WNOHANG)) == 0)
	{
		kill(pid,SIGKILL);
		printf("parent kill child process %d\n",pid);	
	}

	waitpid(pid,NULL,0);
	return 0;
}
执行结果如下:
fs@ubuntu:~/qiang/signal$ ./kill
child(pid : 9977)is waiting for any signal
parent kill child process 9977
fs@ubuntu:~/qiang/signal$ 


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

        alarm() 也称闹钟信号,它可以在进程中设置一个定时器。当定时器指定的时间到时,它就向进程发送SIGALRAM信号。要注意的是,一个进程只能有一个闹钟时间,如果在调用alarm()函数之前已设置过闹钟信号,则任何以前的闹钟时间都被新值所代替。

       pause()函数是用于将调用进程挂起直至收到信号为止

alarm()函数语法:

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

pause() 函数语法

所需头文件 #include
函数原型 int pause(void);
函数返回值 -1;并且把 errno值设为RINTR

下面一个实例,完成一个简单的sleep() 函数的功能,由于SIGALARM 默认的系统动作为终止该进程,因此程序在打印信息之前就已经结束了

执行结果如下:

fs@ubuntu:~/qiang/signal$ ./alarm 
Alarm clock
fs@ubuntu:~/qiang/signal$ 

可以看到printf() 里面的内容并没有被打印, Alarm clock 是SIGALARM信号默认处理函数打印。


3)、信号的设置 signal() 和 sigaction() 

         signal() 函数

        要对一个信号进行处理,就需要给出此信号发生时系统所调用的处理函数。可以为一个特定的信号(除去无法捕捉的SIGKILL和SIGSTOP信号)注册相应的处理函数。如果正在运行的程序源代码里注册了针对某一特定信号的处理程序,不论当时程序执行到何处,一旦进程接收到该信号,相应的调用就会发生。

        signal()函数使用时,只需要指定的信号类型和信号处理函数即可。它主要用于前32种非实时信号的处理,不支持信号传递信息。

其语法格式如下:

所需头文件 #include
函数原型 typeef void (*sighandle_t)(int) ; 函数指针类型
sighandle_t signal(int signum,sighandle_t handler);
函数传入值 signum:指定信号代码
Handler:SIG_IGN:忽略该信号
                  SIG_DFL:采用系统默认方式处理信号
                  自定义的信号处理函数;
函数返回值 成功:以前的信号处理函数
出错:-1

该函数第二个参数和返回值类型都是指向一个无返回值并且带一个整型参数的函数的指针;且只要signal()  调用了自定义的信号处理函数,即使这个函数什么也不做,这个进程也不会被终止;


下面一个程序利用signal来实现发送信号和接受信号的原理:

程序内容:创建子进程代表售票员,父进程代表司机,同步过程如下:

售票员捕捉 SIGINT(代表开车),发送信号SIGUSR1给司机,司机打印(“let's gogogo!”);

售票员捕捉 SIGQUIT(代表停止),发送信号SIGUSR2给司机,司机打印(“stop the bus!”);

司机捕捉 SIGTSTP (代表车到总站),发SIGUSR1给售票员,售票员打印(“Please get off the bus”);

代码如下:

#include 
#include 
#include 
#include 
#include 

pid_t pid;
void driver_handler(int signo);
void saler_handler(int signo);

int main(int argc,char *argv[])
{
	if((pid = fork()) < 0)
	{
		perror("fork error");
		return -1;
	}

	if(pid > 0)
	{
		signal(SIGTSTP,driver_handler);
		signal(SIGINT,SIG_IGN);
		signal(SIGQUIT,SIG_IGN);
		signal(SIGUSR1,driver_handler);
		signal(SIGUSR2,driver_handler);

		while(1)
			pause();
	}

	if(pid == 0)
	{
		signal(SIGINT,saler_handler);
		signal(SIGTSTP,SIG_IGN);
		signal(SIGQUIT,saler_handler);
		signal(SIGUSR1,saler_handler);
		signal(SIGUSR2,SIG_IGN);

		while(1)
			pause();
	}
	return 0;
}

void driver_handler(int signo)
{
	if(signo == SIGUSR1)
		printf("Let's gogogo!\n");
	if(signo == SIGUSR2)
		printf("Stop the bus!\n");
	if(signo == SIGTSTP)
		kill(pid,SIGUSR1);
}

void saler_handler(int signo)
{
	pid_t ppid = getppid();
	if(signo == SIGINT)
		kill(ppid,SIGUSR1);
	if(signo == SIGQUIT)
		kill(ppid,SIGUSR2);
	if(signo == SIGUSR1)
	{
		printf("please get off the bus\n");
		kill(ppid,SIGKILL);
		exit(0);
	}
}

执行结果如下:

fs@ubuntu:~/qiang/signal$ ./signal 
^CLet's gogogo!
^\Stop the bus!
^CLet's gogogo!
^\Stop the bus!
^CLet's gogogo!
^\Stop the bus!
^CLet's gogogo!
^\Stop the bus!
^Zplease get off the bus
Killed
fs@ubuntu:~/qiang/signal$ 

sigaction() 函数

sigaction() 函数的功能是检查或修改(或两者)与指定信号相关联的处理动作,此函数可以完全代替signal 函数。

函数原型如下:

所需头文件 #include
函数原型 int sigaction(int signum,  const struct sigaction *act ,
                                                       struct sigaction *oldact );
函数传入值 signum:可以指定SIGKILL和SIGSTOP以外的所有信号
act        :act 是一个结构体,里面包含信号处理函数的地址、
                  处理方式等信息;
oldact  :参数oldact 是一个传出参数,sigaction 函数调用成功后,
                  oldact 里面包含以前对 signum 信号的处理方式的信息;
函数返回值 成功:0
出错:-1

其中参数signo 是要检测或修改其具体动作的信号编号。若act 指针非NULL,则要修改其动作。如果oact 指针非空,则系统经由 oact 指针返回该信号的上一个动作;

参数结构sigaction定义如下:

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:此参数和signal()的参数handler相同,此参数主要用来对信号旧的安装函数signal()处理形式的支持;

②  sa_sigaction:新的信号安装机制,处理函数被调用的时候,不但可以得到信号编号,而且可以获悉被调用的原因以及产生问题的上下文的相关信息。

③  sa_mask:用来设置在处理该信号时暂时将sa_mask指定的信号搁置;

④  sa_restorer: 此参数没有使用;

⑤  sa_flags:用来设置信号处理的其他相关操作,下列的数值可用。可用OR 运算(|)组合:

ŸA_NOCLDSTOP:如果参数signum为SIGCHLD,则当子进程暂停时并不会通知父进程
SA_ONESHOT/SA_RESETHAND:当调用新的信号处理函数前,将此信号处理方式改为系统预设的方式
SA_RESTART:被信号中断的系统调用会自行重启
SA_NOMASK/SA_NODEFER:在处理此信号未结束前不理会此信号的再次到来
SA_SIGINFO:信号处理函数是带有三个参数的sa_sigaction。

你可能感兴趣的:(Linux系统应用编程)