进程和进程之间无非是通过磁盘、内核、用户空间传输数据。
通过磁盘(也就是文件)实现进程通信这个好理解,服务器进程把计算结果写入文件,客户端进程从文件读数据就可以了。这里的竞争条件是当服务端正在写文件时,客户端是不允许读的。
命名管道把数据写入文件,因此它可以独立于进程存在。但是命名管道是一个队列而非常规的文件,当读者把数据读走后,数据就不存在了,下一次读到的是后面的内容。
普通管道位于内核,用于父子进程间通信,因此它的存在依赖于进程的存在。
进程间通过文件或FIFO传输数据时,write将数据从内存复制到内核缓冲区,read将数据从内核缓冲区复制到内存。而使用共享内存是不存在用户空间和内核空间的来回复制的。共享内存段是用户内存的一部分,不同的进程都拥有指向此内存段的指针。进程依靠访问权限来对这段内存进行读或写。
上面已提到通过文件实现进程间通信时需要对进程进行同步,用到文件锁。
下面的代码服务端负责向文件写入当前时间,客户端读取该文件。
服务端:
#include<sys/file.h> #include<stdio.h> #include<fcntl.h> #include<time.h> #include<stdlib.h> #include<string.h> #include<unistd.h> #include<sys/types.h> #define oops(m,x) {\ perror(m); \ exit(x); \ } main(int argc,char *argv[]){ int fd; time_t now; char* message; if(argc!=2){ fprintf(stderr,"usage:%s filename\n",argv[0]); exit(1); } if((fd=open(argv[1],O_CREAT|O_TRUNC|O_WRONLY,0644))==0) oops(argv[1],2); void lock_opreation(int,int); while(1){ time(&now); message=ctime(&now); if(lseek(fd,0,SEEK_SET)==-1) oops("lseek",3); lock_operation(fd,F_WRLCK); if(write(fd,message,strlen(message))==-1) oops("write",4); lock_operation(fd,F_UNLCK); sleep(1); } } lock_operation(int fd,int op){ struct flock lock; lock.l_whence=SEEK_SET; lock.l_start=0; lock.l_len=0; lock.l_pid=getpid(); lock.l_type=op; if(fcntl(fd,F_SETLKW,&lock)==-1) oops("locl operation",6); }
客户端:
#include<sys/file.h> #include<stdio.h> #include<fcntl.h> #include<time.h> #include<stdlib.h> #include<string.h> #define oops(m,x) {perror(m);exit(x);} #define BUFSIZE 50 main(int argc,char *argv[]){ int fd,nread; char buf[BUFSIZE]; if(argc!=2){ fprintf(stderr,"usage:%s filename\n",argv[0]); exit(1); } if((fd=open(argv[1],O_RDONLY))==-1) oops(argv[1],2); while(1){ lock_operation(fd,F_RDLCK); //读锁 nread=read(fd,buf,BUFSIZE); lock_operation(fd,F_UNLCK); //解除读锁 if(nread>0){ printf("pid %d: %s",getpid(),buf); bzero(buf,BUFSIZE); } lseek(fd,0,SEEK_SET); sleep(3); } close(fd); } lock_operation(int fd,int op){ struct flock lock; lock.l_whence=SEEK_SET; lock.l_start=0; //锁住文件的起始位置 lock.l_len=0; //锁住的长度。0表示到文件结尾 lock.l_pid=getpid(); //哪个进程可能操作该文件 lock.l_type=op; //锁的类型。读、写or解锁 if(fcntl(fd,F_SETLKW,&lock)==-1) //通过fcntl给文件设置锁 oops("lock operation",6); }
使用共享内存的多个进程之间通过信号量来协同工作。信号量是一个内核变量,它是系统级的全局变量,可以被系统中的任何进程所访问。
下面的代码是服务端向共享内存写入当前时间:
#include<stdio.h> #include<sys/shm.h> #include<time.h> #include<sys/types.h> #include<stdlib.h> #include<string.h> #include<sys/sem.h> #include<signal.h> #define TIME_MEM_KEY 99 #define TIME_SEM_KEY 9900 #define SEG_SIZE ((size_t)100) #define oops(m,x) {perror(m);exit(1);} union semnum{ int val; struct semid_ds *buf; //信号量控制结构体 ushort *array; }; int seg_id,semset_id; void clearup(int); main(){ char *mem_ptr,*ctime(); //变量和函数放在一起声明 time_t now; int n; seg_id=shmget(TIME_MEM_KEY,SEG_SIZE,IPC_CREAT|0777); //共享内存有自己单独的读写权限0777 if(seg_id==-1) oops("shmget",1); mem_ptr=shmat(seg_id,NULL,0); if(mem_ptr==(void*)-1) oops("shmat",2); semset_id=semget(TIME_SEM_KEY,2,0666|IPC_CREAT|IPC_EXCL); //产生一个信号量集合,容量为2 if(semset_id==-1) oops("semget",3); set_sem_value(semset_id,0,0); //设置第一个信号量的值 set_sem_value(semset_id,1,0); //设置第二个信号量的值 signal(SIGINT,clearup); //SIGINT信号到来时做一些清理工作 for(n=0;n<60;n++){ time(&now); printf("\tshm_ts2 waiting for lock\n"); wait_and_lock(semset_id); //锁住共享内存 printf("\tshm_ts2 updating memeory\n"); strcpy(mem_ptr,ctime(&now)); //向共享内存写入数据 release_lock(semset_id); //释放信号量 printf("\tshm_ts2 released lock\n"); sleep(1); } clearup(0); } void clearup(int n){ shmctl(seg_id,IPC_RMID,NULL); //删除共享内存 semctl(semset_id,0,IPC_RMID,NULL); //删除信号量 } set_sem_value(int semset_id,int semnum,int val){ union semnum initval; initval.val=val; if(semctl(semset_id,semnum,SETVAL,initval)==-1) oops("semctl",4); } wait_and_lock(int semset_id){ struct sembuf actions[2]; //对信号量进行制作的结构体 actions[0].sem_num=0; //操作第1个信号量 actions[0].sem_flg=SEM_UNDO; //进程结束时,进程对信号量的操作将被撤销(信号量属于IPC对象,IPC对象是独立于进程的) actions[0].sem_op=0; //阻塞直到信号量变为0 actions[1].sem_num=1; actions[1].sem_flg=SEM_UNDO; actions[1].sem_op=+1; //信号量的值加1,表示要释放该信号控制的资源 if(semop(semset_id,actions,2)==-1) oops("semop:locking",10); } release_lock(int semset_id){ struct sembuf actions[1]; actions[0].sem_num=1; actions[0].sem_flg=SEM_UNDO; actions[0].sem_op=-1; //如果当前信号量的值小于1,则阻塞。否则减去1。表示要获得由该信号控制的资源 if(semop(semset_id,actions,1)==-1) oops("semop:unlocking",10); }
管道和socket也包含了锁机制。管道和socket其实也是保存数据的内存段,它将数据从源端复制到目的端。不同的是管道和socket中的锁是由内核,而不是进程来管理的。