一、进程间通信
回顾之前提到的话题,fork()函数被调用一次,都有两个返回值。两次返回值的区别是子进程的返回值是0,父进程的返回值是子进程的进程id。之所以将子进程的进程id返回给父进程是因为:一个进程的子进程可以多于一个,没有一个函数使一个进程可以获得其所有子进程的进程id;对于子进程来说,之所以fork()返回0给他,是因为他可以随时调用getpid()来获取自己的进程号,也可以通过getppid()来获得父进程的进程号。
fork()之后,Linux内核会复制一个一模一样的父进程给子进程,这两个进程共享代码空间,但是数据传递是相互独立的,子进程中的数据空间是父进程的完全copy,指针指令也完全相同,子进程拥有父进程当前运行到的位置(两进程的程序计数器pc值相同,也就是说,子进程是从fork()返回后开始执行的),但有一点不同,如果fork()成功,子进程中fork()的返回值是子进程的进程号,如果fork()不成功,父进程会返回错误。
如果两个进程同时运行,而且步调一致,在fork()之后,他们分别做不同的工作,也就是分叉了。至于哪个先运行,这个与内核进程调度算法有关。如果需要父子进程协同,可以通过原语办法解决。
如果多个进程之间需要协同处理某个任务时,这就需要同步和数据交流。常用的进程间通信(IPC,Inter-Process Communication)的方法有:
1.信号(Sinal):信号是一种比较复杂的方式,用于通知接收进程某个事件已经发生;
2.管道(Pipe):管道是一种半双工工作方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用(通常指父子进程);
3.命名管道FIFO:命名管道也是半双工的工作方式,他允许无亲缘关系进程间的通信;
4.命名socket(UNIX域socket):socket也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同进程间的进程通信;
5.信号量(Semaphore):信号量是一个计数器,可以用来控制多个进程对共享资源的访问。他通常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程间不同线程的同步手段;
6.共享内存(Shared Memory):共享内存就是映射一段能被其他进程访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的IPC方式,它是针对其他进程间通信方式运行效率低而专门设计的。往往和其他通信机制配合使用,来实现进程间的同步和通信;
7.消息队列(Message Queue):消息队列是由消息的链表存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道智能承载无格式字节流以及缓冲区大小受限等缺点;
接下来一一介绍上述几种方式。
二、信号(Signal)
信号是Linux系统中用于进程间通信或操作的一种机制,信号可以在任何时候发送给某一进程,而无需知道该进程的状态。如果该进程处于未执行状态,则该信号由内核保存起来,直到该进程恢复执行并传递给他为止。如果一个信号被进程设置为阻塞,则该信号的传递被延迟,直到其阻塞被取消时才传递给进程。
Linux提供了几十种信号,分别代表不同的意义。可以通过kill -l方式来查看系统支持的信号:
信号是在软件层次上对中断机制的一种模拟,是一种异步通信方式,信号可以在用户空间和内核之间直接交互。内核也可以通过信号告诉用户空间的进程来告诉用户空间发生了哪些系统事件。
我们知道父进程创建子进程之后,究竟是父进程先运行还是子进程先运行,我们并不知道,这取决于操作系统的调度策略。在某些特殊情况下,我们需要确保父子进程运行的先后顺序,则可以使用信号来实现进程间的同步。
下面的程序中,如果父进程先执行则进入到循环休眠状态,直到子进程给他发送信号之后才结束循环,这样就可以确保子进程先执行他的任务。同样地,在子进程执行完任务之后,就等待父进程给他发送信号之后才能退出,而父进程则通过调用wait()系统调用等待子进程退出之后再退出。
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<errno.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<signal.h>
int g_child_stop = 0;
int g_parent_run = 0;void sig_child(int signum)
{
if(SIGUSR1 == signum)
{
g_child_stop = 1;
}
}void sig_parent( int signum )
{
if( SIGUSR2 == signum )
{
g_parent_run = 1;
}
}int main(int argc,char **argv)
{
int pid;
int wstatus;signal(SIGUSR1,sig_child);
signal(SIGUSR2,sig_parent);if( (pid = fork()) < 0 )
{
printf("Create child process failure : %s\n",strerror(errno));return -2;
}else if(pid == 0)
{
/*child process can do something first here,then tell parent process to start running*/printf("Child process start running and send parent a signal\n");
kill(getppid(),SIGUSR2);
while( !g_child_stop )
{
sleep(1);
}printf("Child process receive signal from parent and exit now\n");
return 0;
}printf("Parent hangs up utill receive signal from child\n");
while( !g_parent_run )
{
sleep(1);
}/*Parent process can do something here,then tell child process to exit*/
printf("Parent start running now and send child a signal to exit.\n");
kill(pid,SIGUSR1);
/*Parent wait child process exit*/
wait(&wstatus);
printf("Parent wait child process die and exit now\n");
return 0;
}
三.管道(pipe)
管道是UNIX系统IPC的最古老形式,所有的UNIX系统都提供此种通信机制。管道的实质是一个内核缓冲区,进程以先进先出的方式从管道中读取数据:管道的一端的进程顺序地将数据写入缓冲区,另一端进程则顺序的读取数据,该缓冲区可以看作是一个循环队列,读和写的位置都是自动增加的,一个数据只能被读一次,读出以后的数据在缓冲区都不复存在;当缓冲区读空或者写满的时候,有一定的规则控制相应的读进程或者写进程是否进入等待序列,当空的缓冲区有新数据写入或者慢的缓冲区有数据读出时,就唤醒等待队列中的进程继续读写。
1.管道的局限性:
(1)半双工的工作方式:数据只能是一个方向流动;
(2)管道只能在有亲缘关系间的进程内使用;
2.管道的创建:
管道的创建可以看成是一种特殊的文件,对于他的读写也可以使用普通的read、write等函数,但他不是普通的文件,并不属于其他任何文件系统,并且只存在于内存之中
#include<unistd.h>
int pipe(int fd[2]);//返回值:如果成功,返回0;失败,返回-1;
经由参数fd返回的两个文件描述符:fd[0]为读而打开,fd[1]为写而打开
通常进程会先调用pipe(),在调用fork(),从而创建了子进程与父进程的IPC通道。
fork()之后做什么取决于我们想要的数据流的方向,对于从父进程到子进程,父进程关闭管道的读端fd[0],子进程关闭写端fd[1];
3.关闭管道的一端
(1)当第一个写端被关闭的管道,在所有数据都被读取后,read返回0,表示文件结束;
(2)当一个读端被关闭的管道时,则产生信号SIGPIPE,如果忽略该信号或者捕捉该信号并从其处理程序返回,则write返回-1;
下面编写一个程序,用于父进程给子进程方向发送数据。首先,父进程创建管道之后fork(),这时子进程会继承父进程所有打开的文件描述符(包括管道),这时对于一个管道就有4个读写端(父子进程分别各有一个读写端),如果需要父进程往子进程里写数据,则关闭父进程写端,然后关闭子进程读端;
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<errno.h>
#include<sys/types.h>
#include<sys/wait.h>#define MSG_STR "This message is from parent : Hello child process!"
int main(int argc,char **argv)
{int pipe_fd[2];
int rv;
int pid;
char buf[512];
int wstatus;
/*创建管道*/
if( pipe(pipe_fd) < 0 )
{
printf("Create pipe failure : %s\n",strerror(errno));
return -1;
}/*创建子进程*/
if( (pid = fork()) < 0 )
{
printf("Create child process failure : %s\n",strerror(errno));
return -2;
}else if(pid == 0)
{
/* child process close write endpoint,then read data from parent process */
close(pipe_fd[1]);memset(buf,0,sizeof(buf));
rv = read(pipe_fd[0],buf,sizeof(buf));
if(rv < 0)
{
printf("Child process read from pipe failure : %s\n",strerror(errno));
return -3;
}printf("Child process read %d bytes data from pipe :\"%s\"\n",rv,buf);
return 0;
}/* parent process close read endpoint then write data to child process*/
close(pipe_fd[0]);
if( write(pipe_fd[1],MSG_STR,strlen(MSG_STR)) < 0 )
{
printf("Parent process write data to pipe failure : %s\n",strerror(errno));
return -3;
}printf("Parent process start wait child process exit...\n");
wait(&wstatus);
return 0;
}
四、命名管道(FIFO)
前面已经讲到了未命名的管道只能用于具有亲缘关系的进程之间通信;但是通过命名管道FIFO,不相关的进程也能交换数据。FIFO不同于管道之处在于他提供一个路径与之关联,以FIFO的文件形式存在于系统中。他在磁盘上有对应的节点,但是没有数据块,换个说法,命名管道只是拥有一个名字和相应的访问权限,通过mknode()系统调用或者mkfifo()函数来建立。一旦建立,任何进程都可以通过文件名将其打开进行读写,而不局限于父子进程,但是也要具有对FIFO适当的访问权。当不再被进程使用时,FIFO在内存中释放,但磁盘节点仍然存在。
下面这个程序创建了两个隐藏命名的管道文件(.fifo_chat1和.fifo_chat2)在不同的进程间进行通信。该程序需要运行两次(即两个进程),其中进程0(mode = 0)从标准输入里读取数据后通过命名管道2写入数据给进程1(mode = 1);而进程1则从标准输入里读取数据后通过管道1写给进程0,这样使用命名管道就实现了一个进程间聊天的程序
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<errno.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<sys/stat.h>
#include<libgen.h>
#include<stdlib.h>
#include <fcntl.h>#define FIFO_FILE1 ".fifo_chat1"
#define FIFO_FILE2 ".fifo_chat2"int g_stop = 0;
void sig_pipe(int signum)
{
if(SIGPIPE == signum)
{
printf("get pipe broken signal and let programe exit.\n");
g_stop = 1;
}
}int main(int argc,char **argv)
{
int fdr_fifo;
int fdw_fifo;
int rv;
fd_set rdset;
char buf[1024];
int mode = 0;
if(argc != 2)
{
printf("Usage:%s [0/1]\n",basename(argv[0]));
printf("This chat program need run twice,1st time run with [0] and 2nd time with [1]\n");
return -1;
}mode = atoi(argv[1]);
/*管道是一种半双工的通信方式,如果要实现两个进程间的双向通信则需要两个管道,即两个管道分别作为两个进程的读端和写端*/
if( access(FIFO_FILE1,F_OK) )
{
printf("FIFO file \"%s\" not exist and create it now\n",FIFO_FILE1);
mkfifo(FIFO_FILE1,0666);
}if( access(FIFO_FILE2,F_OK) )
{
printf("FIFO file \"%s\" not exist and create it now\n",FIFO_FILE2);
mkfifo(FIFO_FILE2,0666);
}signal(SIGPIPE,sig_pipe);
if( 0 == mode )
{
/*这里以只读模式打开命名管道FIFO_FILE1的读端,默认是阻塞模式;如果命名管道的写端不被打开则open()将会一直阻塞,
* 所以另外一个进程必须首先以写模式打开该文件FIFO_FILE1,否则会出现死锁*/printf("start open '%s' for read and it will blocked untill write endpoint opened...\n",FIFO_FILE1);
if( (fdr_fifo = open(FIFO_FILE1,O_RDONLY)) < 0 )
{
printf("Open fifo[%s] for chat read endpoint failure : %s\n",FIFO_FILE1,strerror(errno));
return -1;
}printf("start open '%s' for write...\n",FIFO_FILE2);
if( (fdw_fifo = open(FIFO_FILE2,O_RDONLY)) < 0)
{
printf("Open fifo[%s] for chat write endpoint failure : %s\n",FIFO_FILE2,strerror(errno));
return -1;
}
}else
{
/*这里以只写模式打开命名管道FIFO_FILE1的写端,默认是阻塞模式;如果命名管道的读端不被打开则open()将会一直阻塞,
* 因为前一个进程先是以读模式打开该管道文件的读端,所以这里必须先以写模式打开该文件的写端,否则会出现死锁*/printf("start open '%s' for write and it will blocked untill read endpoint opened...\n",FIFO_FILE1);
if( (fdw_fifo = open(FIFO_FILE1,O_WRONLY)) < 0 )
{
printf("Open fifo[%s] for chat write endpoint failure :%s\n",FIFO_FILE1,strerror(errno));return -1;
}
printf("start open '%s' for read...\n",FIFO_FILE2);
if( (fdr_fifo = open(FIFO_FILE2,O_RDONLY)) < 0 )
{
printf("Open fifo[%s] for chat read endpoint failure : %s\n",FIFO_FILE2,strerror(errno));
return -1;
}
}
printf("start chating with another program now,please input message now:\n");while(!g_stop)
{
FD_ZERO(&rdset);
FD_SET(STDIN_FILENO,&rdset);
FD_SET(fdr_fifo,&rdset);
/*select 多路复用监听标准输入和作为输入的命名管道读端*/rv = select(fdr_fifo+1,&rdset,NULL,NULL,NULL);
if( rv <= 0 )
{
printf("Select get timeout or error:%s\n",strerror(errno));
continue;
}
/*如果是作为输入的命名管道上有数据到来则从管道上读入数据并打印到标准输出上*/if( FD_ISSET(fdr_fifo,&rdset) )
{
memset(buf,0,sizeof(buf));
rv = read(fdr_fifo,buf,sizeof(buf));if( rv < 0 )
{
printf("read data form FIFO get error:%s\n",strerror(errno));
break;
}else if(0 == rv)
{
printf("another side of FIFO get closed and program will exit now\n");
break;
}printf("<-- %s",buf);
}/*如果标准输入上有数据到来,则标准输入上读入数据后,将数据写入稿作为输出命名管道上给另外一个进程*/
if( FD_ISSET(STDIN_FILENO,&rdset) )
{
memset(buf,0,sizeof(buf));
fgets(buf,sizeof(buf),stdin);
write(fdw_fifo,buf,strlen(buf));
}
}
}
五、命名socket
使用socket除了可以实现网络间不同主机间通信外,还可以实现同一主机的不同进程间的通信,且建立的通信时双向的通信,这种方式就叫做命名socket(或Unix域socket)。Unix域协议并不是一个实际的协议族,而是在单个主机上执行客户/服务器通信的一种方式,也是进程间通信(IPC)的一种方式。UNIX域协议是可靠的,不会丢失消息,也不会传递出错。他提供了两类套接字:字节流套接字、数据报套接字。
命名socket与普通的TCP/IP网络socket相比具有以下特点:
(1)UNIX域套接字与传统的套接字的区别是用路径名表示协议族的描述,ls -l看到的该文件的类型是s
(2)UNIX域套接字与套接字相比,在同一台主机的传输速度前者是后者的两倍,UNIX域套接字仅仅复制数据,并不执行协议处理,不需要天机或删除网络报头,无需计算校验和,不产生顺序号,也不需要发送确认报文
(3)UNIX域套接字可以在同一台主机上各进程之间传递文件描述符
命名socket与TCP/IP网络socket通信使用的是同一套接口,只是地址结构与某些参数不同:TCP/IP网络socket通过IP地址和端口号来表示,而UNIX域协议中使用普遍文件系统路径名标识。所以命名socket只是在创建socket和绑定服务器表示的时候与socket有区别:具体如下:
int socket(int domain,int type,int protocol);
说明:创建一个socket,可以是TCP/IP网络的socket,也是命名socket,具体有domain确定。该函数的返回值为生成的套接字描述符。
domain:指定协议族,对于命名socket,其值需被设置为AF_UNIX或AF_LOCAL;如果是网络socket应该被设置为AF_INET
type:指定套接字类型,他可以被设置为SOCK_STREAM(流式套接字)或SOCK_STREAM(数据报式套接字)
protocol:设置为0
SOCK_STREAM式本地套接字的通信双方均需要具有本地地址,其中服务器端的本地地址需要明确指定,指定方法是使用struct sockaddr_un类型的变量。
struct sockaddr_un {
sa_family_t sun_family; /* AF_UNIX */
char sun_path[UNIX_PATH_MAX]; /* 路径名 */
命名socket的命名方式有两种。一种是普通的命名,socket会根据此命名创建一个同名的socket文件,客户端连接的时候通过读取该socket文件连接到socket服务端。这种方式的缺点是服务端必须对socket文件的路径具备写权限,客户端必须知道socket文件路径名,且必须对该路径有读权限。另一种命名方式是抽象命名空间,这种方式不需要创建socket文件,只需要命名一个全局名字,即可让客户端根据此名字进行连接。后者的实现过程与前者的差别是,后者在对地址结构成员sun_path数组赋值的时候,必须把第一个字节置0,即sun_path[0] = 0。这里第一种方式比较常见,下面例程中采取第一种方法。
命名socket服务器端和客户端通信的代码:
#include<stdio.h>
#include<string.h>
#include<errno.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<sys/un.h>#define SOCKET_PATH "/tmp/socket.domain"
int main(int argc,char **argv)
{
int rv = -1;
int listen_fd,client_fd = -1;
struct sockaddr_un serv_addr;
struct sockaddr_un cli_addr;
socklen_t cliaddr_len;
char buf[1024];listen_fd = socket(AF_LOCAL,SOCK_STREAM,0);
if(listen_fd < 0)
{
printf("create socket failure : %s\n",strerror(errno));
return -1;
}printf("socket create fd[%d]\n",listen_fd);
if( !access(SOCKET_PATH,F_OK) )
{
unlink(SOCKET_PATH);
}memset(&serv_addr,0,sizeof(serv_addr));
serv_addr.sun_family = AF_UNIX;
strncpy(serv_addr.sun_path,SOCKET_PATH,sizeof(serv_addr.sun_path)-1);if( bind(listen_fd,(struct sockaddr *)&serv_addr,sizeof(serv_addr)) < 0 )
{
printf("create socket failure : %s\n",strerror(errno));
unlink(SOCKET_PATH);
return -2;
}printf("socket[%d] bind on path \"%s\" ok\n",listen_fd,SOCKET_PATH);
listen(listen_fd,13);
while(1)
{
printf("\nStart waiting and accept new client connect...\n",listen_fd);
client_fd = accept(listen_fd,(struct sockaddr *)&cli_addr,&cliaddr_len);if( client_fd < 0 )
{
printf("accept new socket failure : %s\n",strerror(errno));
return -2;
}memset(buf,0,sizeof(buf));
if( (rv = read(client_fd,buf,sizeof(buf))) <0 )
{
printf("Read data from client socket[%d] failure :%s\n",client_fd,strerror(errno));
close(client_fd);
continue;
}
else if(rv == 0)
{
printf("client socket[%d] disconnected\n",client_fd);
close(client_fd);
continue;
}printf("Read %d bytes data from client[%d] and echo it back : '%s'\n",rv,client_fd,buf);
if( write(client_fd,buf,rv) < 0 )
{
printf("write %d bytes data back to client[%d] failure :%s\n",
rv,client_fd,strerror(errno));
close(client_fd);
}
sleep(1);
close(client_fd);
}close(listen_fd);
}
#include<stdio.h>
#include<string.h>
#include<errno.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<sys/un.h>
#define SOCKET_PATH "/tmp/socket.domain"
#define MSG_STR "Hello,Unix Domain Socket Server!"
int main(int argc,char **argv)
{
int conn_fd = -1;
int rv = -1;
char buf[1024];
struct sockaddr_un serv_addr;conn_fd = socket(AF_UNIX,SOCK_STREAM,0);
if(conn_fd < 0)
{
printf("create socket failure : %s\n",strerror(errno));
return -1;
}memset(&serv_addr,0,sizeof(serv_addr));
serv_addr.sun_family = AF_UNIX;
strncpy(serv_addr.sun_path,SOCKET_PATH,sizeof(serv_addr.sun_path)-1);if( connect(conn_fd,(struct sockaddr *)&serv_addr,sizeof(serv_addr)) < 0 )
{
printf("connect to unix domain socket server on \"%s\" failure : %s\n",
SOCKET_PATH,strerror(errno));
return 0;
}printf("connect to unix domain socket server on \"%s\" ok\n",SOCKET_PATH);
if( write(conn_fd,MSG_STR,strlen(MSG_STR)) < 0 )
{
printf("write data to unix domain socket server on \"%s\" failure : %s\n",
SOCKET_PATH,strerror(errno));
goto cleanup;
}
memset(buf,0,sizeof(buf));
rv = read(conn_fd,buf,sizeof(buf));if( rv < 0)
{
printf("Read data from server failure : %s\n",strerror(errno));
goto cleanup;
}else if(0 == rv)
{
printf("Client connect to server get disconnected\n");
goto cleanup;
}printf("Read %d bytes data from server : '%s'\n",rv,buf);
cleanup:
close(conn_fd);
}