Interprocess communication IPC:
We introduced the process control primitive before and saw how to create multiple processes. However, the way of exchanging information between processes only introduces inheriting the open file of the parent process through fork or exec or through the file system.
Classic process communication methods include: pipeline, FIFOs, message queue, semaphore and shared memory.
1. Piping:
PIPE is the oldest form of IPC in Unix system, and PIPE has the following limitations:
1) It is half duplex. Some systems provide full duplex pipes, but for portability, we'd better not make this assumption.
2) Pipes can only be between processes with a common ancestor. Generally, a process creates a pipeline, then the process calls fork, and then the pipeline is used between the parent process and the child process.
FIFO gets rid of the second limitation. Unix domain socket and named streams based pipeline can get rid of these two limitations.
Despite the above limitations, half duplex pipes are still the most commonly used IPC.
1) Create pipe:
#include <unistd.h> int pipe(int filedes[2]);
After the call is successful, two file descriptors are returned through filedes: filedes[0] is opened for reading, filedes[1] is opened for writing, and the output of filedes[1] is the input of filedes[0]. The fstat function returns that the two file descriptors are FIFO through macro S_ISFIFO to determine whether it is a pipeline.
The pipeline of a process is almost useless. Usually a process calls pipe to create pipes, then calls fork to create a IPC channel between the parent and child processes.
The pipeline from the parent process to the child process. The parent process closes the pipeline at the reading end (filedes[0]) and the child process closes the pipeline at the writing end (filedes[1]); The pipeline from the child process to the parent process. The parent process closes filedes[1], and the child process closes filedes[0]
When the pipe at one end is closed:
1) If we read from a closed pipeline, when all the data has been read, read returns 0, indicating the end of the file.
2) If we write from a pipeline whose read segment has been closed, a SIGPIPE signal will be generated. When we ignore or process it, write returns - 1 and errno is set to EPIPE.
When we write data into the pipe, the constant PIPE_BUF indicates the buffer size of the kernel pipeline. When multiple processes write to the pipeline, the write is less than or equal to PIPE_BUF size data will not be interleaved and larger than PIPE_BUF may cause data interleaving. We can use sysconf to view the pipe_ The size of the buf.
example:
#include <unistd.h> #include <sys/types.h> #include <stdlib.h> #define MAXLINE 90 int main(void){ int n; int fd[2]; pid_t pid; char line[MAXLINE]; if(pipe(fd) < 0){ perror("pipe"); exit(1); } if((pid = fork()) < 0){ perror("fork"); exit(1); }else if(pid > 0){ close(fd[0]); write(fd[1],"hello,world\n",12); }else{ close(fd[1]); n = read(fd[0],line,MAXLINE); write(STDOUT_FILENO,line,n); } exit(0); }
This example creates a pipe from the parent process to the child process and sends some data.
We copy the file descriptor of pipe to standard input and output, which will be more interesting. We often run our program, read from standard input or write to standard output.
example:
We consider writing a program to display some output, one page at a time. We can use the paging program provided by the system. We avoid writing data to temporary files or using system. We want to establish a pipeline from standard input to paging.
#include <unistd.h> #include <sys/types.h> #include <stdlib.h> #include <sys/wait.h> #include <stdio.h> #define DEF_PAGER "/bin/more" #define MAXLINE 80 int main(int argc,char *argv[]){ int n; int fd[2]; pid_t pid; char *pager, *argv0; char line[MAXLINE]; FILE *fp; if(argc != 2){ printf("Usage: a.out <pathname>"); exit(1); } if((fp = fopen(argv[1],"r")) == NULL){ perror("fopen"); exit(1); } if(pipe(fd) < 0){ perror("pipe"); exit(1); } if((pid = fork()) < 0){ perror("fork"); exit(1); }else if(pid > 0){/* parent */ close(fd[0]);/* close read end */ while(fgets(line,MAXLINE,fp) != NULL){ n = strlen(line); if(write(fd[1],line,n) != n){ perror("write"); exit(1); } } if(ferror(fp)){ perror("fgets"); exit(1); } close(fd[1]); /* close write end of the pipe for reader */ if(waitpid(pid,NULL,0) < 0){ perror("waitpid"); exit(1); } exit(0); }else{/* child */ close(fd[1]); if(fd[0] != STDIN_FILENO){ if(dup2(fd[0],STDIN_FILENO) != STDIN_FILENO){ perror("dup2"); exit(1); } close(fd[0]);// don't need it after dup2 } /* get arguments for execl() */ if((pager = getenv("PAGER")) == NULL){ pager = DEF_PAGER; } if((argv0 = strrchr(pager,'/')) != NULL) argv0++;/* step past rightmost slash */ else argv0 = pager; if(execl(pager,argv0,(char *)0) < 0){ perror("execl"); exit(1); } } exit(1); }
2) popen and pclose functions:
Since there are some common operations, such as creating a pipe from the current process to another process, reading its output or sending data to its input, the standard I/O supports popen and pclose
These two functions do a lot of dirty work for us: create a pipe, fork a sub process, close the end of the useless pipe, execute the shell to run the command, and finally wait for the execution of the command to terminate.
#include <stdio.h> FILE *popen(const char *cmdstring, const char *type); int pclose(FILE *fp);
popen executes a fork and exec to execute cmdstring and return the standard I/O file pointer. If the type is "r", the file pointer is connected to the standard output of cmdstring.
If the type is "w", it is connected to the standard input of cmdstring.
A good way to remember is to compare with fopen, "r" means readable and "w" means writable.
pclose closes the standard I/O, waits for the command execution to terminate, and returns to the execution state of the shell.
We use popen to rewrite the above example:
#include <unistd.h> #include <sys/types.h> #include <stdlib.h> #include <sys/wait.h> #include <stdio.h> #define MAXLINE 90 #define PAGER "${PAGER:-more}" int main(int argc, char** argv){ char line[MAXLINE]; FILE *fpin, *fpout; if(argc != 2){ printf("usage: a.out <pathname>\n"); exit(1); } if((fpin = fopen(argv[1],"r")) == NULL){ perror("fopen"); exit(1); } if((fpout = popen(PAGER,"w")) == NULL){ perror("popen"); exit(1); } while(fgets(line,MAXLINE,fpin) != NULL){ if(fputs(line,fpout) == EOF){ perror("fputs"); exit(1); } } if(ferror(fpin)){ perror("fgets"); exit(1); } if(pclose(fpout)){ perror("pclose"); exit(1); } exit(0); }
2,FIFOs:
FIFOs are often referred to as named pipes. Pipes can only be used between processes with a common ancestor. FIFO can exchange data between unrelated processes.
FIFO is a file type, which can be accessed through S_ISFIFO to test it.
Creating a FIFO is similar to creating a file.
1) Create a FIFO:
#include <sys/stat.h> int mkfifo(const char *path, mode_t mode;);
0 is returned for success, - 1 is returned for failure, and errno is set.
example:
#define FIFO_PERMS (S_IRUSR | S_IWUSR | S_IRGRP | S _IROTH) if(mkfifo("myfifo",FIFO_PERMS) == -1) perror("Failed to create myfifo");
Deleting FIFO is the same as deleting ordinary files.
Example: the parent process reads the data written by the child process to the named pipe:
#include <errno.h> #include <fcntl.h> #include <stdio.h> #include <string.h> #include <unistd.h> #include <sys/stat.h> #include <sys/wait.h> #define BUFSIZE 256 #define FIFO_PERM (S_IRUSR | S_IWUSR) int dofifochild(const char *fifoname, const char *idstring); int dofifoparent(const char *fifoname); int main(int argc,char *argv[]){ pid_t childpid; if(argc != 2){ fprintf(stderr,"Usage: %s pipename\n",argv[0]); return 1; } if(mkfifo(argv[1],FIFO_PERM) == -1){ if(errno != EEXIST){ fprintf(stderr,"[%ld]: failed to create name pipe %s: %s\n", (long)getpid(),argv[1],strerror(errno)); return 1; } } if((childpid = fork()) == -1){ perror("Failed to fork"); return 1; } if(childpid == 0){ return dofifochild(argv[1],"this was written by the child"); }else{ return dofifoparent(argv[1]); } } int dofifochild(const char *fifoname, const char *idstring){ char buf[BUFSIZE]; int fd; int rval; ssize_t strsize; fprintf(stderr,"[%ld]:(child) about to open FIFO %s...\n",(long)getpid(),fifoname); if(fd = open(fifoname,O_WRONLY) == -1){ fprintf(stderr,"[%ld]:failed to open name pipe %s for write: %s\n", (long)getpid(),fifoname,strerror(errno)); return 1; } rval = snprintf(buf,BUFSIZE,"[%ld]:%s\n",(long)getpid(),idstring); if(rval < 0){ fprintf(stderr,"[%ld]: failed to make the string:\n",(long)getpid()); return 1; } strsize = strlen(buf)+1; fprintf(stderr,"[%ld]:about to write...\n",(long)getpid()); rval = write(fd,buf,strsize); if(rval != strsize){ fprintf(stderr,"[%ld]:failed to write to pipe: %s\n",(long)getpid(),strerror(errno)); return 1; } fprintf(stderr,"[%ld]:finishing...\n",(long)getpid()); return 0; } int dofifoparent(const char *fifoname) { char buf[BUFSIZE]; int fd; int rval; fprintf(stderr, "[%ld]:(parent) about to open FIFO %s...\n", (long)getpid(), fifoname); if ((fd = open(fifoname, FIFO_PERM)) == -1) { fprintf(stderr, "[%ld]:failed to open named pipe %s for read: %s\n", (long)getpid(), fifoname, strerror(errno)); return 1; } fprintf(stderr, "[%ld]:about to read...\n", (long)getpid()); rval = read(fd, buf, BUFSIZE); if (rval == -1) { fprintf(stderr, "[%ld]:failed to read from pipe: %s\n", (long)getpid(), strerror(errno)); return 1; } fprintf(stderr, "[%ld]:read %.*s\n", (long)getpid(), rval, buf); return 0; }
After gcc compilation, it runs, where fifo1 is the name we give the named pipeline
m@ubuntu:~/test$ ./a.out fifo1 [2721]:(parent) about to open FIFO fifo... [2722]:(child) about to open FIFO fifo... [2722]:about to write... [2722]:this was written by the child [2722]:finishing... [2721]:about to read... [2721]:read m@ubuntu:~/test$ file fifo1 fifo1: fifo (named pipe)
2) Using FIFO client server communication:
We use a simple protocol to communicate between the client and the server. The client writes the log information to the named pipe, and the server reads it from the named pipe and then writes it to the file:
Server:
#include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/stat.h> #include <errno.h> #define FIFOARG 1 #define BLKSIZE 1024 #define FIFO_PERMS (S_IRWXU | S_IWGRP | S_IWOTH) int main(int argc, char *argv[]){ int requestfd; if(argc != 2){ fprintf(stderr,"Usage: %s fifoname > logfile\n",argv[0]); return 1; } if((mkfifo(argv[FIFOARG],FIFO_PERMS) == -1) && (errno != EEXIST)){ perror("Server failed to create FIFO"); return 1; } if((requestfd = open(argv[FIFOARG],O_RDWR)) == -1){ perror("Server failed to open its FIFO"); return 1; } copyfile(requestfd,STDOUT_FILENO); return 1; } int copyfile(int fromfd, int tofd) { char *bp; char buf[BLKSIZE]; int bytesread, byteswritten; int totalbytes = 0; for ( ; ; ) { while (((bytesread = read(fromfd, buf, BLKSIZE)) == -1) && (errno == EINTR)) ; /* handle interruption by signal */ if (bytesread <= 0) /* real error or end-of-file on fromfd */ break; bp = buf; while (bytesread > 0) { while(((byteswritten = write(tofd, bp, bytesread)) == -1 ) && (errno == EINTR)) ; /* handle interruption by signal */ if (byteswritten <= 0) /* real error on tofd */ break; totalbytes += byteswritten; bytesread -= byteswritten; bp += byteswritten; } if (byteswritten == -1) /* real error on tofd */ break; } return totalbytes; }
client:
#include <errno.h> #include <fcntl.h> #include <limits.h> #include <stdio.h> #include <stdlib.h> #include <strings.h> #include <time.h> #include <unistd.h> #include <sys/stat.h> #define FIFOARG 1 int main(int argc, char *argv[]){ time_t curtime; int len; char requestbuf[PIPE_BUF]; int requestfd; if(argc != 2){ fprintf(stderr,"Usage %s fifoname", argv[0]); return 1; } if((requestfd = open(argv[FIFOARG],O_WRONLY)) == -1){ perror("Client failed to open log fifo for writing"); return 1; } curtime = time(NULL); snprintf(requestbuf,PIPE_BUF,"%d:%s",(int)getpid(),ctime(&curtime)); len = strlen(requestbuf); if(write(requestfd,requestbuf,len) != len){ perror("Client failed to write"); return 1; } close(requestfd); return 0; }
reference resources:
- <Unix system programming>
- <Advanced Programming in the Unix Environment>