1. 为什么进程间需要通信
1)数据传输
一个进程需要将它的数据发送给另一个进程。
2)资源共享
多个进程之间共享同样的资源。
3)通知事件
一个进程需要向另一个或一组进程发送消息,通知它们发生了某种事件。
4)进程控制
有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有操作,并能够及时知道它的状态变化。
2. Linux进程间通信(IPC)由以下几部分发展而来:
1)UNIX进程间通信
2)基于System V进程间通信
3)POSIX进程间通信
3. Linux使用的进程间通信方式包括:
1)管道(pipe)和有名管道(FIFO)
2)信号(signal)
3)消息队列
4)共享内存
5)信号量
6)套接字(socket)
4. 管道通信
管道是单向的、先进先出的,它把一个进程的输出和另一个进程的输入连接在一起。一个进程(写进程)在管道的尾部写入数据,另一个进程(读进程)从管道的头部读出数据。
数据被一个进程读出后,将被从管道中删除,其它读进程将不能再读到这些数据。管道提供了简单的流控制机制,进程试图读空管道时,进程将堵塞。同样,管道已经满时,进程再试图向管道写入数据,进程将阻塞。
管道包括无名管道,用于父进程和子进程间的通信;命名管道,用于运行于同一系统中的任意两个进程间的通信。
4.1 无名管道
由pipe()函数创建:
#include
int pipe(int pipefd[2]);
当一个管道建立时,它会建立两个文件描述符,存放在pipefd数组中:filedis[0]用于读管道,filedis[1]用于写管道。
返回值:成功,返回0;失败,返回-1,并设置errno。
关闭管道只需将这两个文件描述符关闭即可,可以使用普通的close函数逐个关闭。
必须在系统调用fork()前调用pipe(),否则子进程将不会继承文件描述符。
无名管道例子:
#include#include #include #include #include #include //memset
int main()
{
int pipe_fd[2];
pid_t pid;
char buf_r[100];
char *p_wbuf;
int r_num;
memset(buf_r,0,sizeof(buf_r));
//新建管道
if(pipe(pipe_fd)<0)
{
printf("pipe create error\n");
return -1;
}
pid=fork();//新建子进程
if(pid==0)//子进程OR父进程
{
printf("\n");
close(pipe_fd[1]);
sleep(2);//休眠,等待父进程写入数据
if((r_num=read(pipe_fd[0],buf_r,100))>0)
{
printf("%d numbers read from the pipe is %s\n",r_num,buf_r);
}
close(pipe_fd[0]);
exit(0);
}
else if(pid>0)
{
close(pipe_fd[0]);
if(write(pipe_fd[1],"Hello",5)!=-1)
printf("parent write1 Hello!\n");
if(write(pipe_fd[1]," Pipe",5)!=-1)
printf("parent write2 Pipe!\n");
close(pipe_fd[1]);
sleep(3);
waitpid(pid,NULL,0);//等待子进程结束
exit(0);
}
return 0;
}
命名管道和无名管道基本相同,但也有不同点:无名管道只能由父子进程使用;但是通过命名管道,不相关的进程也能交换数据。
4.2 命名管道
#include
#include
int mkfifo(const char *pathname, mode_t mode);
参数:pathname:FIFO文件名,mode:属性(同文件操作),文件的权限为(mode&~umask)
一旦创建了一个FIFO文件,任何进程都能打开它,来进行读/写,与操作普通文件一样。
返回值:成功,返回0;失败,返回-1并设置errno。
命名管道例子:
/*fifo_read.c*/
#include
int main(int argc,char** argv) { char buf_r[100]; int fd; int nread;
unlink(FIFO);//先删除FIFO,防止重新建立失败 if(mkfifo(FIFO,0666)==-1) { printf("cannot create fifo\n"); exit(1); }
printf("Preparing for reading bytes...\n");
memset(buf_r,0,sizeof(buf_r));//数组清零
fd=open(FIFO,O_RDONLY|O_NONBLOCK,0);//非阻塞方式打开 if(fd==-1) { perror("open"); exit(1); } while(1) { memset(buf_r,0,sizeof(buf_r));
if((nread=read(fd,buf_r,100))==-1) { if(errno==EAGAIN) printf("no data yet\n"); } printf("read %s from FIFO\n",buf_r); sleep(1); } pause();//暂停,等待信号 }
/*fifo_write.c*/
#include
int main(int argc,char *argv[]) { int fd; char w_buf[100]; int nwrite;
fd=open(FIFO_SERVER,O_WRONLY|O_NONBLOCK,0); if(fd==-1) { printf("cannot open FIFO_SERVER\n"); exit(1); }
if(argc==1) { printf("Please send something\n"); exit(-1); }
strcpy(w_buf,argv[1]);//将传递的参数放到w_buf中
if((nwrite=write(fd,w_buf,100))==-1) { if(errno==EAGAIN) printf("The FIFO has not been read yet.Please try later\n"); } else printf("write %s to the FIFO\n",w_buf); return 0; }
$ gcc fifo_read.c -o read $ gcc fifo_write.c -o write 在终端1中运行: $ ./read Preparing for reading bytes... read from FIFO 在终端2中运行: $ ./write 789456 write 789456 to the FIFO 然后在终端1中有: read 789456 from FIFO read from FIFO
5 信号通信
5.1 信号产生
信号(signal)机制是Unix系统中最为古老的进程间通信机制,很多条件可以产生一个信号:
1)当用户按某些按键时,产生信号。
2)硬件异常产生信号:除数为0、无效的存储访问等等。这些情况通常由硬件检测到,将其通知内核,然后内核产生适当的信号通知进程,例如,内核对正在访问一个无效存储区的进程产生一个SIGSEGV信号。
3)进程用Kill函数将信号发送给另一个进程。
4)用户可用kill命令将信号发送给其他进程。
5.2 信号类型
下面是几种常见的信号:
* SIGHUP:从终端上发出的结束信号
* SIGINT:来自键盘的终端信号(Ctrl-C)
* SIGKILL:该信号结束接受信号的进程
* SIGTERM:kill命令发出的信号
* SIGCHLD:标识子进程停止或结束的信号
* SIGSTOP:来自键盘(Ctrl-Z)或调试程序的停止执行信号
5.3 信号处理
当某信号出现时,将按照下列三种方式中的一种进行处理:
1)忽略此信号
大多数信号都按照这种方式进行处理,但有两种信号却决不能被忽略。它们是:SIGKILL和SIGSTOP。它们向超级用户提供了一种终止或停止进程的方法。
2)执行用户希望的动作
通知内核在某种信号发生时,调用一个用户函数。在用户函数中,执行用户希望的处理。
3)执行系统默认动作
对大多数信号的系统默认动作是终止该进程。
5.4 信号的发送
发送信号的函数主要有kill和raise。区别:kill既可以向自身发送信号,也可以向其他进程发送信号。与kill函数不同的是,raise函数是向进程自身发送信号。
#include
#include
int kill(pid_t pid, int sig);
kill系统调用能发送任何信号给任何进程组和进程。
参数:pid=0,发送sig给进程号为pid的进程;pid=0,发送sig给调用进程组中的每个进程;pid=-1,发送sig给有权限发送信号的所有进程,除了进程1(init);pid<-1,发送sig给组进程ID=-pid的所有进程。
参数:sig=0,不发送任何信号,但是错误检查依然有效,可以用来检查组进程ID和进程ID的存在。
返回值:成功,返回0;失败,返回-1,并设置errno。
#include
int raise(int sig);
raise()函数发送一个信号给调用进程或线程。在单线程中,它等同于:
kill(gitpid(),sig);
多线程中,它等同于:
pthread_kill(pthread_self(), sig);
返回值:成功,返回0,失败,返回非零值。
#include
unsigned int alarm(unsigned int seconds);
使用alarm函数可以设置一个时间值(闹钟时间),当所设置的时间到了时,产生SIGALRM信号。如果不捕捉此信号,则默认动作是终止该进程。
参数:seconds,指定的等待秒数,为0时,不使用。
返回值:当预定的alarm还没有交付时,返回剩下的秒数;当没有预定的alarm时,返回0
#include
int pause(void);
pause()函数使调用进程(线程)休眠直到捕捉到一个信号。该信号会终止进程或是调用一个信号捕捉函数。
返回值:当信号被捕捉或信号捕捉函数返回时,pause()会返回-1,并设置errno为EINTR。
5.5 信号的处理
当系统捕捉到某个信号时,可以忽略信号或是使用指定的处理函数来处理该信号,或者使用系统默认的方式。
信号处理的主要方法有两种,一种是使用简单的signal函数,另一种是使用信号集函数组。
信号处理函数:
#include
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
handler可能的值是:
1)SIG_IGN:忽略此信号
2)SIG_DFL:按系统默认方式处理
3)信号处理函数名:使用该函数处理
信号通信例子:
#include
void my_func(int sign_no) { if(sign_no==SIGINT) printf("I have get SIGINT\n"); else if(sign_no==SIGQUIT) printf("I have get SIGQUIT\n"); }
int main() { printf("Waiting for signal SIGINT or SIGQUIT \n ");
//注册处理函数 signal(SIGINT, my_func); signal(SIGQUIT, my_func);
pause(); exit(0); }
在终端1中: $ gcc mysignal.c -o signal $ ./signal Waiting for signal SIGINT or SIGQUIT 等待信号,在终端2中查询该进程的PID,用如下命令: $ ps aux | grep "./signal" user 12950 0.0 0.0 1536 348 pts/0 S+ 19:49 0:00 ./signal $ kill -s SIGINT 12950 OR $ kill -s SIGQUIT 12950 终端1中有: I have get SIGINT OR I have get SIGQUIT
6. 共享内存
6.1 共享内存
是被多个进程共享的一部分物理内存。共享内存是进程间共享数据的一种最快的方法,一个进程向共享内存区域写入了数据,共享这个内存区域的所有进程就可以立刻看到其中的内容。
6.2 共享内存实现
1)创建共享内存,使用shmget函数。
2)映射共享内存,将这段创建的共享内存映射到具体的进程空间去,使用shmat函数。
6.3 操作函数
#include
#include
int shmget(key_t key, size_t size, int shmflg);
功能:shmget()函数返回与参数key有关的共享内存段的标识符,当key的值是IPC_PRIVATE时,或者key的值为0,shmflg为IPC_CREAT时,新的内存段的大小是PAGE_SIZE整数倍的size。如果shmflg同时指定为IPC_CREAT | IPC_EXCL并且共享内存段key已经存在了,shmget()会失败并设置errno为EEXIST。
参数:shmflg,为IPC_CREAT时,创建一个新的段,如果不是用这个标识,shmget()会查找key段并检查用户是否有访问该段的权限。IPC_EXCL,和IPC_CREAT一起使用,当段已经存在时,确认失败。
返回值:成功时,返回一个合法的段标识符shmid;失败,返回-1。
#include
#include
void *shmat(int shmid, const void *shmaddr, int shmflg);
连接共享内存标识符shmid和调用进程的地址空间shmaddr。
参数:shmid,共享内存标识符。shmaddr,地址空间。flag,决定以什么方式来确定映射的地址(通常为0)。
返回值:成功,返回共享内存映射到进程中的地址;失败,返回-1。
int shmdt(const void *shmaddr);
解除映射
#include
#define PERM S_IRUSR|S_IWUSR//可读,可写
int main(int argc,char *argv[]) { int shmid; char *p_addr,*c_addr;
if(argc!=2) { fprintf(stderr,"Usage:%s\n\a",argv[0]); exit(1); }
if((shmid=shmget(IPC_PRIVATE,1024,PERM))==-1) { fprintf(stderr,"Create Share Memory Error:%s\n\a",strerror(errno)); exit(1); }
//创建子进程 if(fork())//父进程写 { p_addr=shmat(shmid,0,0); memset(p_addr,'\0',1024); printf("Server put %s\n",argv[1]); strncpy(p_addr,argv[1],1024); wait(NULL); exit(0); } else { sleep(1);//暂停1s c_addr=shmat(shmid,0,0); printf("Client get %s\n",c_addr); exit(0); } }
7. 消息队列 Unix早期的通信机制之一——信号能够传送的信息量有限,管道则只能传送无格式的字节流,这无疑会给应用程序开发带来不便。消息队列(也叫报文队列)则克服了这些缺点。 消息队列就是一个消息的链表。可以把消息看作一个记录,具有特定的格式。进程可以按照一定的规则向它添加新的消息;另一些进程则可以从消息队列中读走消息。 目前主要有两种类型的消息队列:POSIX消息队列和系统V消息队列,系统V消息队列目前被大量使用。系统V消息队列是随内核持续的,只有在内核重启或人工删除时,该消息队列才会被删除。消息队列的内核持续性要求每个消息队列都在系统范围内对应唯一的键值,所以,要获得一个消息队列的描述字,必须提供该消息队列的键值。
7.1 键值
#include
#include
key_t ftok(const char *pathname, int proj_id);
功能:使用给定的pathname(必须是存在的并且可以访问的)的标识符和至少8位有效位的proj_id(非零)来产生一个V IPC的key。
参数:pathname,文件名;proj_id,项目名(不为0即可)。
返回值:成功,返回生成的key_t;失败,返回-1。
7.2 获取消息队列的描述符
#include
#include
#include
int msgget(key_t key, int msgflg);
功能:返回与key参数相关的消息队列的标识符。如果key的值是IPC_PRIVATE或key的值不是IPC_PRIVATE而msgflg的值是IPC_CREAT时,创建消息队列。
参数:msgflg的取值,IPC_CREAT,创建新的消息队列;IPC_EXCL与IPC_CREAT一同使用,表示如果要创建的消息队列已经存在,则msgget()会失败并设置errno为EEXIST。IPC_NOWAIT,读写消息队列要求无法得到满足时,不阻塞。
返回值:成功,返回消息队列标识符(非负整形);失败,返回-1。
7.2 发送消息
#include
#include
#include
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
功能:用来给消息队列发送或则从消息队列接受消息。调用该函数的进程必须有写权限来对消息队列进行消息发送或读权限来接受消息。
参数:msqid消息队列标识符;msgp指向由调用方定义的结构体,其一般形式如下:
struct msgbuf {
long mtype; /* message type, must be > 0 */
char mtext[1]; /* message data */
};
域mtex是一个矩阵,它的大小由非负整型数msgsz来指定。允许消息的长度为0。域mtype必须为正整数,该值可被接受进程用来进行消息选择。
msgsnd()系统调用能将msgp指向的消息的副本追加到标识符为msqid的消息队列中。如果队列中有足够的空间,msgsnd()会立即成功,如果队列中没有足够的空间,msgsnd()的默认动作是阻塞直到有足够的空间。如果在msgflg中指定了IPC_NOWAIT,调用失败。
msgrcv()系统调用能将标识符为msqid的消息队列中的msgtyp类型的消息移除并存放在指针msgp指向的缓冲区。参数msgsz指定参数msgp指向的结构体成员mtext的最大字节数。
返回值:成功,msgsnd()返回0,msgrcv()返回复制到数组mtext中的字节数;失败,返回-1,并设置errno。
消息队列例子:
#include
struct msg_buf { int mtype; char data[255]; };
int main() { key_t key; int msgid; int ret; struct msg_buf msgbuf;
key=ftok("./hello",'a'); printf("key=[%x]\n",key); msgid=msgget(key,IPC_CREAT|0666);/*通过文件对应*/
if(msgid==-1) { printf("create error\n"); return -1; }
if(fork()>0)//创建子进程,在父进程中时 { msgbuf.mtype = 1;//给类型赋值,必须与接受函数匹配 strcpy(msgbuf.data,"test haha"); ret=msgsnd(msgid,&msgbuf,sizeof(msgbuf.data),IPC_NOWAIT); if(ret==-1) { printf("send message err\n"); return -1; } wait(NULL); } else//子进程中 { sleep(1); memset(&msgbuf,0,sizeof(msgbuf)); ret=msgrcv(msgid,&msgbuf,sizeof(msgbuf.data),1,IPC_NOWAIT); if(ret==-1) { printf("recv message err\n"); return -1; } printf("recv msg =\"%s\"\n",msgbuf.data); return 0; } return 0; }
在当前目录下建立文件hello,并编译源文件: $ touch hello $ gcc msg.c -o msg $ ./msg key=[6101a2f8] recv msg ="test haha"
8. 信号量
信号量(又名:信号灯)与其他其它进程间通信方式大不相同,主要用途是保护临界资源。进程可以根据它判断是否能够访问某些共享资源。除了用于访问控制外,还可用于进程同步。
#include
#include
#include
int semget(key_t key, int nsems, int semflg);
功能:返回与key参数相关的信号集的标识符。如果key的值是IPC_PRIVATE或key的值不是IPC_PRIVATE而msgflg的值是IPC_CREAT时,创建新的信号集的中的信号个数为nsems。
参数:semflg的取值,IPC_EXCL与IPC_CREAT一同使用,表示如果要创建的信号集已经存在,则semget()会失败并设置errno为EEXIST。
返回值:成功,返回信号集标识符(非负整数);失败,返回-1,并设置errno。
#include
#include
#include
int semop(int semid, struct sembuf *sops, unsigned nsops);
int semtimedop(int semid, struct sembuf *sops, unsigned nsops, struct timespec *timeout);
功能:semop()选定由semid指示的信号量。由指针sops指向的,位于矩阵中的nsops个元素中的每一个都对应于一个单信号量的操作。这个结构体的元素是结构体sembuf类型的,包含如下元素:
struct sembuf{
unsigned short sem_num; /* semaphore number */
short sem_op; /* semaphore operation */
short sem_flg; /* operation flags */
}
sem_flg标志有两个元素,IPC_NOWAIT和SEM_UNDO,如果操作指定为SEM_UNDO,在进程结束时,将会自动地释放信号量。
返回值:成功,返回0;失败,返回-1。