Linux系统编程(3)——进程间通信


原文链接1: https://blog.csdn.net/qq_43519025/article/details/120798727
原文链接2: https://blog.csdn.net/qq_46323094/article/details/117441666

一、前言

  • 进程间通信(IPC,Interprocess communication)是一组编程接口,让程序员能够协调不同的进程,使之能在一个操作系统里同时运行,并相互传递、交换信息。这使得一个程序能够在同一时间里处理许多用户的要求。因为即使只有一个用户发出要求,也可能导致一个操作系统中多个进程的运行,进程之间必须互相通话。IPC接口就提供了这种可能性。每个IPC方法均有它自己的优点和局限性,一般,对于单个程序而言使用所有的IPC方法是不常见的。

二、进程间通信有哪几种方式

无名管道、FIFO也称有名管道、消息队列、共享内存、信号、信号量。

三、无名管道

  1. 特点:
    1.1 是一种半双工的通信方式,因为一方想进行写入或者读取数据时,都需要先关闭对方的通道;
    1.2 只能在具有亲缘关系的进程间使用,进程的亲缘关系一般指的是父子关系;
    1.3 它可以看成是一种特殊的文件,对于它的读写也可以使用普通的read、write等函数。但是它不是普通的文件,并不属于其他任何文件系统,并且只存在于内存中。

  2. 函数原型:

    #include <unistd.h>
    int pipe(int pipefd[2]);
    
  3. 源码

    #include <unistd.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    
    int main(void)
    {
    	int fd[2];
    	char readbuf[128] = {0};
    
    	int pi_ret = pipe(fd);
    
    	if(pi_ret < 0) {
    		printf("creat IPC failed\n");
    	}
    	else {
    		if(fork() > 0) {
    			printf("this is child process\n");
    			close(fd[1]);
    			read(fd[0], readbuf, 128);
    			printf("readbuf = %s\n", readbuf);
    			exit(0);
    		}
    		else {
    			printf("this is father process\n");
    			close(fd[0]);
    			write(fd[1], "hello world", strlen("hello world"));
    			wait(NULL);
    		}
    	}
    
    	return 0;
    }
    

四、FIFO(有名管道)

  1. 特点
    1.1 它是一种文件类型;但也是半双工通信,因为不管是读取还是写入时都会阻塞。
    1.2 FIFO可以在无关的进程之间交换数据,与无名管道不同;
    1.3 FIFO有路径名与之相关联,它以一种特殊设备文件形式存在于文件系统中。

  2. 函数原型

    #include <sys/types.h>
    #include <sys/stat.h>
    int mkfifo(const char *pathname, mode_t mode);
    

    2.1 其中的 mode 参数与 open 函数中的 mode 相同。一旦创建了一个 FIFO,就可以用一般的文件 I/O 函数操作它。
    2.2 当 open 一个 FIFO 时,是否设置非阻塞标志(O_NONBLOCK)的区别:

     若没有指定 O_NONBLOCK(默认),只读 open 要阻塞到某个其他进程为写而打开此 FIFO。类似的,只写 open 要阻塞到某个其他进程为读而打开它。
     若指定了 O_NONBLOCK,则只读 open 立即返回。而只写 open 将出错返回 -1 如果没有进程已经为读而打开该 FIFO,其 errno 置 ENXIO。
     使用 mkfifo 创建FIFO文件,filename为文件的路径,mode为文件权限,在这里要注意掩码对于创建文件权限的影响。创建失败返回-1,如果FIFO文件已存在,errno值为EEXIST。
    
  3. 源码
    write.c

    #include <unistd.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <errno.h>
    #include <fcntl.h>
    
    int main(void)
    {
    	int fd;
    	if((mkfifo("./FIFO", 0600) == -1) && (errno != EEXIST)) {
    		printf("creat fifo failed\n");
    		perror("why:");
    	}
    	fd = open("./FIFO", O_WRONLY);
    	printf("open write success\n");
    
    	while(1) {
    		write(fd, "from IPC", strlen("from IPC"));
    		sleep(1);
    	}
    
    	close(fd);
    
    	return 0;
    }
    

    read.c

    #include <unistd.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <errno.h>
    #include <fcntl.h>
    
    int main(void)
    {
    	int fd;
    	char buf[30] = {0};
    	if((mkfifo("./FIFO", 0600) == -1) && (errno != EEXIST)) {
    		printf("creat fifo failed\n");
    		perror("why:");
    	}
    	fd = open("./FIFO", O_RDONLY);
    	printf("open read success\n");
    
    	while(1) {
    		read(fd, buf, 30);
    		printf("readbuf = %s\n", buf);
    	}
    
    	return 0;
    }
    

五、消息队列

  1. 说明
    1.1 消息队列,是消息的链接表,存放在内核之中。一个消息队列由一个标识符(即队列ID)来标识,每个消息队列中,又根据不同的消息Type来区分数据。
    1.2 用户进程可以向消息队列添加消息,也可以向消息队列读取消息。
  2. 特点
    2.1 消息队列是面向记录的,其中的消息具有特定的格式以及特定的优先级;
    2.2 消息队列是独立于发送和接收进程的,进程终止时,消息队列及其内容并不会被删除;
    2.3 消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取。
    2.4 消息队列是IPC中的一种全双工通信方式。
  3. 消息队列函数的原型:
    // 创建或打开消息队列:成功返回队列ID,失败返回-1
    int msgget(key_t key, int flag);
    // 添加消息:成功返回0,失败返回-1
    int msgsnd(int msqid, const void *ptr, size_t size, int flag);
    // 读取消息:成功返回消息数据的长度,失败返回-1
    int msgrcv(int msqid, void *ptr, size_t size, long type,int flag);
    // 控制消息队列:成功返回0,失败返回-1
    int msgctl(int msqid, int cmd, struct msqid_ds *buf);
    
  4. 源码
    write.c
    #include <sys/types.h>
    #include <sys/ipc.h>
    #include <sys/msg.h>
    #include <stdio.h>
    #include <unistd.h>
    #include <stdlib.h>
    //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);
    
    //int msgget(key_t key, int msgflg);
    
    //int msgctl(int msqid, int cmd, struct msqid_ds *buf);
    
    struct msgbuf {
            long mtype;       /* message type, must be > 0 */
    	char mtext[128];    /* message data */
    };
    
    
    int main(void)
    {
    	key_t key;
    	struct msgbuf readbuf = {0};
    	struct msgbuf sendbuf = {888, "hello world" };
    
    	key = ftok(".", 1);
    
    	int msgId = msgget(key, IPC_CREAT|0777);
    	if(msgId == -1) {
    		printf("magget error\n");
    		exit(-1);
    	}	
    
    	msgsnd(msgId, &sendbuf, sizeof(sendbuf.mtext), 0);
    
    	
    	msgrcv(msgId, &readbuf, sizeof(readbuf.mtext), 889, 0);
        printf("readbuf2 = %s\n", readbuf.mtext);
    
    	msgctl(msgId, IPC_RMID, NULL);
    
    	return 0;
    }
    
    read.c
    #include <sys/types.h>
    #include <sys/ipc.h>
    #include <sys/msg.h>
    #include <stdio.h>
    #include <unistd.h>
    #include <stdlib.h>
    //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);
    
    //int msgget(key_t key, int msgflg);
    
    //int msgctl(int msqid, int cmd, struct msqid_ds *buf);
    
    struct msgbuf {
            long mtype;       /* message type, must be > 0 */
    	char mtext[128];    /* message data */
    };
    
    
    int main(void)
    {
    	key_t key;
    
    	struct msgbuf readbuf = {0};
    	struct msgbuf sendbuf = {889, "hello world2" };
    	key = ftok(".", 1);
    
        int msgId = msgget(key, IPC_CREAT|0777);
    	if(msgId == -1) {
    		printf("magget error\n");
    		exit(-1);
    	}	
    
    	msgrcv(msgId, &readbuf, sizeof(readbuf.mtext), 888, 0);
    	printf("readbuf = %s\n", readbuf.mtext);
    
    
    	msgsnd(msgId, &sendbuf, sizeof(sendbuf.mtext), 0);
    
    	msgctl(msgId, IPC_RMID, NULL);
    
    	return 0;
    }
    

六、共享内存

  1. 说明
    1.1 共享内存,指两个或多个进程共享一个给定的存储区。
    1.2 ipcs -m 查看系统下已有的共享内存;ipcrm -m shmid可以用来删除共享内存。
  2. 特点:
    2.1 共享内存是最快的一种 IPC,因为进程是直接对内存进行存取。
    2.2 因为多个进程可以同时操作,所以需要进行同步。
    2.3 信号量 + 共享内存通常结合在一起使用,信号量用来同步对共享内存的访问。
  3. 共享内存函数的原型:
    // 创建或获取一个共享内存:成功返回共享内存ID,失败返回-1
    int shmget(key_t key, size_t size, int flag);
    // 连接共享内存到当前进程的地址空间:成功返回指向共享内存的指针,失败返回-1
    void *shmat(int shm_id, const void *addr, int flag);
    // 断开与共享内存的连接:成功返回0,失败返回-1
    int shmdt(void *addr); 
    // 控制共享内存的相关信息:成功返回0,失败返回-1
    int shmctl(int shm_id, int cmd, struct shmid_ds *buf);
    
  4. 源码
    write.c
    #include <stdio.h>
    #include <stdlib.h>
    #include <sys/types.h>
    #include <sys/ipc.h>
    #include <sys/shm.h>
    #include <string.h>
    //int shmget(key_t key, size_t size, int shmflg);
    //void *shmat(int shmid, const void *shmaddr, int shmflg);
    //int shmdt(const void *shmaddr);
    //int shmctl(int shmid, int cmd, struct shmid_ds *buf);
    
    
    int main(void)
    {
    	key_t key;
    	char *shmaddr;
    
    	key = ftok(".", 1);
    	
    	int shmId = shmget(key, 1024*4, IPC_CREAT|0666);
    	if(shmId == -1)
    	{
    		printf("creat failed\n");
    		exit(-1);
    	}
    
    	shmaddr = (char *)shmat(shmId, 0, 0);
    	strcpy(shmaddr, "jiangyo");
    
    	sleep(5);
    	shmdt(shmaddr);	
    	shmctl(shmId, IPC_RMID, 0);
    
    	return 0;
    }
    
    read.c
    #include <stdio.h>
    #include <stdlib.h>
    #include <sys/types.h>
    #include <sys/ipc.h>
    #include <sys/shm.h>
    #include <string.h>
    //int shmget(key_t key, size_t size, int shmflg);
    //void *shmat(int shmid, const void *shmaddr, int shmflg);
    //int shmdt(const void *shmaddr);
    //int shmctl(int shmid, int cmd, struct shmid_ds *buf);
    
    
    int main(void)
    {
    	key_t key;
    	char *shmaddr;
    
    	key = ftok(".", 1);
    	
    	int shmId = shmget(key, 1024*4, 0);
    	if(shmId == -1)
    	{
    		printf("creat failed\n");
    		exit(-1);
    	}
    
    	shmaddr = (char *)shmat(shmId, 0, 0);
    	printf("buf = %s\n", shmaddr);
    
    	shmdt(shmaddr);	
    
    	return 0;
    }
    

七、信号

  1. 说明
    1.1 对于 Linux来说,实际信号是软中断,许多重要的程序都需要处理信号。终端用户输入了 ctrl+c 来中断程序,会通过信号机制停止一个程序。
    1.2 每个信号都有一个名字和编号,这些名字都以“SIG”开头。我们可以通过kill -l来查看信号的名字以及序号。
    在这里插入图片描述
    1.3 不存在0信号,kill对于0信号有特殊的应用。
    1.4 信号相关函数中的初级函数,只涉及到信号捕捉和发送;高级函数的发送函数中可以给结构体赋值,发送信号的同时,把结构体也会携带一起发送,接受函数就能够接受数据。
  2. 信号的处理:
    2.1 信号的处理有三种方法,分别是:忽略、捕捉和默认动作。
    • 忽略信号,大多数信号可以使用这个方式来处理,但是有两种信号不能被忽略(分别是 SIGKILL和SIGSTOP);
    • 捕捉信号,需要告诉内核,用户希望如何处理某一种信号,说白了就是写一个信号处理函数,然后将这个函数告诉内核。当该信号产生时,由内核来调用用户自定义的函数,以此来实现某种信号的处理。
    • 系统默认动作,对于每个信号来说,系统都对应由默认的处理动作,当发生了该信号,系统会自动执行。具体的信号默认动作可以使用man 7 signal来查看系统的具体定义。
  3. 信号处理函数的注册:
    //入门版:函数
    signal()
    //高级版:函数
    sigaction()
    
  4. 信号处理发送函数:
    //入门版:
    kill()
    //高级版:
    sigqueue()
    
  5. 入门版函数原型:
     //接收函数,第二个参数指向信号处理函数
    sighandler_t signal(int signum, sighandler_t handler);
    //发送函数
     int kill(pid_t pid, int sig);
    
  6. 入门版函数signal()源码
    #include <signal.h>
    #include <stdio.h>
    
    //typedef void (*sighandler_t)(int);
    
    //sighandler_t signal(int signum, sighandler_t handler);
    
    void handler(int num)
    {
    	switch(num)
    	{
    		case 2:
    			printf("SIGINT\n");
    			break;
    		case 9:
    			printf("SIGKILL\n");
    			break;
    		case 10:
    			printf("SIGUSR1\n");
    			break;
    		default :
    			break;
    	}
    	printf("never quit\n");
    }
    
    
    int main(void)
    {
    	signal(SIGINT, handler);
    	signal(SIGKILL, handler);
    	signal(SIGUSR1, handler);
    
    	while(1);
    
    	return 0;
    }
    
  7. 入门版kill()函数源码
    #include <signal.h>
    #include <stdio.h>
    #include <sys/types.h>
    //typedef void (*sighandler_t)(int);
    
    //sighandler_t signal(int signum, sighandler_t handler);
    
    
    //int kill(pid_t pid, int sig);
    
    
    int main(int argc, char **argv)
    {
    	int pid, sig;
    //	char cmd[128] = {0};
    	sig = atoi(argv[1]);
    	pid = atoi(argv[2]);
    
    	kill(pid, sig);
    
    //	sprintf(cmd, "kill -%d %d", sig, pid);
    
    //	system(cmd);
    
    	return 0;
    }
    
  8. 高级版函数原型
    #include <signal.h>
    int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
     
    struct sigaction {
       void       (*sa_handler)(int); //信号处理程序,不接受额外数据,SIG_IGN 为忽略,SIG_DFL 为默认动作
       void       (*sa_sigaction)(int, siginfo_t *, void *); //信号处理程序,能够接受额外数据和sigqueue配合使用
       sigset_t   sa_mask;//阻塞关键字的信号集,可以再调用捕捉函数之前,把信号添加到信号阻塞字,信号捕捉函数返回之前恢复为原先的值。
       int        sa_flags;//影响信号的行为SA_SIGINFO表示能够接受数据
     };
    //回调函数句柄sa_handler、sa_sigaction只能任选其一
    //我们只需要配置 sa_sigaction以及sa_flags即可。
    
    //接受处理函数中的数据结构体
    siginfo_t {
                   int      si_signo;    /* Signal number */
                   int      si_errno;    /* An errno value */
                   int      si_code;     /* Signal code */
                   int      si_trapno;   /* Trap number that caused
                                            hardware-generated signal
                                            (unused on most architectures) */
                   pid_t    si_pid;      /* Sending process ID */
                   uid_t    si_uid;      /* Real user ID of sending process */
                   int      si_status;   /* Exit value or signal */
                   clock_t  si_utime;    /* User time consumed */
                   clock_t  si_stime;    /* System time consumed */
                   sigval_t si_value;    /* Signal value */
                   int      si_int;      /* POSIX.1b signal */
                   void    *si_ptr;      /* POSIX.1b signal */
                   int      si_overrun;  /* Timer overrun count; POSIX.1b timers */
                   int      si_timerid;  /* Timer ID; POSIX.1b timers */
                   void    *si_addr;     /* Memory location which caused fault */
                   int      si_band;     /* Band event */
                   int      si_fd;       /* File descriptor */
    }
    
    //发送信号函数
    #include <signal.h>
    int sigqueue(pid_t pid, int sig, const union sigval value);
    union sigval {
       int   sival_int;
       void *sival_ptr;
     };
    
  9. 高级版函数sigqueue()源码
    #include <stdio.h>
    #include <signal.h>
    #include <sys/types.h>
    #include <unistd.h>
    //int sigqueue(pid_t pid, int sig, const union sigval value);
    
    int main(int argc, char **argv)
    {
    	int num;
    	pid_t pid;
    
    	union sigval sig;
    
    	sig.sival_int = 100;
    
    	num = atoi(argv[1]);
    	pid = atoi(argv[2]);
    
    	sigqueue(pid, num, sig);
    
    	printf("send pid = %d\n", getpid());
    
    	return 0;
    }
    
  10. 高级版函数sigaction()源码
    #include <stdio.h>
    #include <signal.h>
    #include <sys/types.h>
    #include <unistd.h>
    
    //int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
    void handler(int num, siginfo_t *info, void *buf)
    {
    	printf("sig num = %d\n", num);
    
    	if(buf != NULL) {
    		printf("data = %d\n", info->si_int);
    		printf("send pid = %d\n", info->si_pid);
    	}
    }
    
    int main(void)
    {
    	struct sigaction act = {0};
    
    	printf("read pid = %d\n", getpid());
    
    	act.sa_sigaction = handler;
    	act.sa_flags = SA_SIGINFO;
    
    	sigaction(SIGUSR1, &act, NULL);
    
    	while(1);
    
    	return 0;
    }
    

八、信号量

  1. 说明
    信号量与已经介绍过的 IPC 结构不同,它是一个计数器。信号量用于实现进程间的互斥与同步,而不是用于存储进程间通信数据。
  2. 特点
    2.1 信号量用于进程间同步,若要在进程间传递数据需要结合共享内存。
    2.2 信号量基于操作系统的 PV 操作,程序对信号量的操作都是原子操作。
    2.3 每次对信号量的 PV 操作不仅限于对信号量值加 1 或减 1,而且可以加减任意正整数。
    2.4 支持信号量组
  3. 信号量的函数原型:
    // 创建或获取一个信号量组:若成功返回信号量集ID,失败返回-1
    //num_sems表示创建几个信号量,sem_flags权限
    int semget(key_t key, int num_sems, int sem_flags);
    // 对信号量组进行操作,改变信号量的值:成功返回0,失败返回-1
    //semid表示semget函数的返回值,numops操作几个信号量
    int semop(int semid, struct sembuf semoparray[], size_t numops);  
    // 控制信号量的相关信息
    int semctl(int semid, int sem_num, int cmd, ...);
    
    • 当 semget 创建新的信号量集合时,必须指定集合中信号量的个数(即 num_sems),通常为 1; 如果是引用一个现有的集合,则将 num_sems 指定为 0 。
    • 在 semop 函数中,sembuf 结构的定义如下:
      struct sembuf 
      {
          short sem_num; // 信号量组中对应的序号,0~sem_nums-1
          short sem_op;  // 信号量值在一次操作中的改变量
          short sem_flg; // IPC_NOWAIT, SEM_UNDO
      }
      
    • 在 semctl 函数中的命令有多种,这里就说两个常用的:
      • SETVAL:用于初始化信号量为一个已知的值。
      • IPC_RMID:删除一个信号量集合。如果不删除信号量,它将继续在系统中存在,即使程序已经退出,它可能在你下次运行此程序时引发问题,而且信号量是一种有限的资源。
  4. 源码
    #include <stdio.h>
    #include <sys/types.h>
    #include <sys/ipc.h>
    #include <sys/sem.h>
    #include <unistd.h>
    //int semget(key_t key, int nsems, int semflg);
    
    //int semctl(int semid, int semnum, int cmd, ...);
    
    //int semop(int semid, struct sembuf *sops, unsigned nsops);
    
    union semun{
    	int              val;    /* Value for SETVAL */
    	struct semid_ds *buf;    /* Buffer for IPC_STAT, IPC_SET */
    	unsigned short  *array;  /* Array for GETALL, SETALL */
    	struct seminfo  *__buf;  /* Buffer for IPC_INFO
    								(Linux-specific) */
    };
    
    void pGetKey(int semid)
    {
    	struct sembuf sop = {0};
    	sop.sem_num = 0;        信号量组中对应的序号,0~sem_nums-1
    	sop.sem_op = -1;        
    	sop.sem_flg = SEM_UNDO;	// IPC_NOWAIT, SEM_UNDO
    
    	semop(semid, &sop, 1);
    	printf("get key\n");
    }
    
    void vPutBackKey(int semid)
    {
    	struct sembuf sop = {0};
    	sop.sem_num = 0;        信号量组中对应的序号,0~sem_nums-1
    	sop.sem_op = 1;            
    	sop.sem_flg = SEM_UNDO; // IPC_NOWAIT, SEM_UNDO
    
    	semop(semid, &sop, 1);
    	printf("put key\n");
    }
    
    int main(void)
    {
    
    	key_t key;
    	int semId;
    	if((key == ftok(".",6)) < 0)
    	{
    			printf("ftok error\n");
    	}
    
    	semId = semget(key , 1,  IPC_CREAT|0666);//创造钥匙,数量为1
    
    	union semun semm;
    	semm.val = 0;//初始状态为没有钥匙
    
    
    	//0:control 1 key
    	semctl(semId, 0, SETVAL, semm);
    
    	pid_t pid = fork();
    
    	if(pid>0){
    		pGetKey(semId);
    		printf("this is father\n");
    		vPutBackKey(semId);
    		semctl(semId, 0, IPC_RMID);
    	}
    	else if(pid == 0) {
    		printf("this is child\n");
    		vPutBackKey(semId);
    	}
    	else {
    		printf("creat error\n");
    	}
    	
    
    	return 0;
    }
    

九、进程间通信方式总结:

  1. 管道:速度慢,容量有限,只有父子进程能通讯;
  2. FIFO:任何进程间都能通讯,但速度慢;
  3. 消息队列:容量受到系统限制,且要注意第一次读的时候,要考虑上一次没有读完数据的问题;
  4. 共享内存:能够很容易控制容量,速度快,但要保持同步,比如一个进程在写的时候,另一个进程要注意读写的问题;
  5. 信号:有入门版和高级版两种,区别在于入门版注重动作,高级版可以传递消息。只有在父子进程或者是共享内存中,才可以发送字符串消息;
  6. 信号量:不能传递复杂消息,只能用来同步。用于实现进程间的互斥与同步,而不是用于存储进程间通信数据。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值