前言
Linux环境下,进程地址空间相互独立、彼此隔离,因此进程间的数据不能直接访问。如果要交换数据,必须要通过内核,在内核中开辟一块缓冲区,进程A把数据从用户空间拷贝到内核缓冲区,进程B再把数据从内核缓冲区拷贝走,内核提供的这种机制称为进程间通信(IPC, InterProcess Communication)。
在Linux下由很多种进程间通信的方式,分别是:匿名管道(PIPE)、命名管道(FIFO)、信号、共享内存、消息队列、信号量、UNIX域套接字。现在常用的进程间通信方式有:
管道(使用简单)
信号(开销最小)
共享内存(效率最高)
UNIX域套接字(最稳定)
1 信号
上面说的进程间通信,都是常规状态下的工作模式。对于异常情况下的工作模式,就需要用「信号」的方式来通知进程。
信号是进程间通信机制中唯一的异步通信机制,它是由用户、系统或进程发送给目标进程的信息,以通知目标进程某个状态的改变或系统异常。但所能携带的信息有限,只是通知其他进程一个信号,而不能发送具体的数据。我们先看一下进程结构体中关于信号的字段。
struct task_struct {
...
// 收到的信号
long signal;
// 每个信号对应的处理函数和一些标记
struct sigaction sigaction[32];
// 当前屏蔽的信号
long blocked;
};
在shell终端输入kill -l 来查看支持的信号:
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX
Linux信号可由下面的条件产生:
对于前台进程,用户可以输入特殊终端字符来发送,比如输入Ctrl+C通常会给正在运行的进程发送一个中断信号
系统异常,比如浮点异常和非法内存段访问
系统状态变化,比如alarm定时器到期时将引起SIGALRM信号
在终端运行kill命令或在程序中调用kill函数,例如:如果要杀死一个进程,我们可以使用 kill -9 pid 来杀死进程,-9表示发送9号信号,也就是SIGKILL信号,pid为发送信号的目标进程的进程ID
信号的机制:
进程或用户A给一个进程B发送信号,B在收到信号之前执行自己的代码,当B进程收到信号后,不管程序执行到什么位置,都要暂停运行,去处理信号,也就是调用信号处理函数,处理完再继续执行。与硬件中断类似——异步模式。但信号是软件层面实现的中断,早期常被成为“软中断”。
信号的特质:
由于信号是通过软件方法实现,其实现手段导致信号有很强的延时性。但对于用户来说,这个延迟时间非常短,不易察觉。每个进程收到的所有信号,都是由内核负责发送的,内核处理。
2 匿名管道(PIPE)
匿名管道是进程间通信中比较简单的一种,他只用于有继承关系的进程之间。进一步来说,子进程可以使用继承于父进程的资源。管道通信的原理如下。
父子进程通过fork后,子进程继承了父进程的文件描述符。所以他们指向同一个数据结构。父子进程通常只需要单向通信,父子进程各关闭自己的一端。当父子进程对管道进程读写的时候,操作系统会控制这一切,包括数据的读取和写入,进程的挂起和唤醒。
dmesg | grep mipi
其中”|“是管道的意思,它的作用就是把前一条命令的输出作为后一条命令的输入 ,两个进程要进行通信的话,就可以用这种管道来进行通信了,并且这条竖线是没有名字的,所以我们把这种通信方式称之为匿名管道。
父子进程通过管道通信例程:
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
int main() {
int fds[2];
pid_t pid;
int ret;
char buf[1024];
if (pipe(fds) < 0) {
perror("open pipe error:");
exit(1);
}
pid = fork();
if (pid < 0) {
perror("fork error:");
exit(2);
} else if (pid > 0) { // parend process
// parent process write to pipe
close(fds[0]); // close read end
ret = write(fds[1], "I am your father", strlen("I am your father"));
if (ret < 0) {
perror("parent write pipe error:");
exit(3);
}
printf("parent process id:%u, son:%u, write success\n", getpid(), pid);
wait(NULL);
} else { // child process
// child process read from pipe
close(fds[1]); // close wirte end
ret = read(fds[0], buf, sizeof(buf));
if (ret < 0) {
perror("child read pipe error:");
exit(3);
}
buf[ret] = '\0';
printf("child process id:%u, father:%u, read from pipe:%s\n", getpid(), getppid(), buf);
}
return 0;
}
管道读写的注意事项:
读管道:
管道中有数据,读出数据,返回读到的字节数
管道中无数据:①如果写端被关闭,read返回0 ②写端没有关闭,read阻塞
写管道:
管道读端全部关闭:进程异常终止(收到SIGPIPE信号,最好捕捉SIGPIPE信号,是进程不终止)
管道读端没有全部关闭:①管道已满,write阻塞 ②管道未满,将数据写入,返回写入的字节数
匿名管道的优劣:
优点:使用简单
缺点:①只能单向通信 ②只能用于父子、兄弟进程间通信
3 命名管道(FIFO)
FIFO常被称为命名管道,以区分管道(PIPE)。PIPE只能用于有"血缘关系"的进程间通信,但FIFO可以在不相关的进程间实现通信。
FIFO是Linux基础文件中的一种,但FIFO在磁盘上没有数据块,仅仅用来标识内核中的一条通道。各进程可以打开这个文件进行read/write,实际上是在读写内核通道,这样就实现了进程间通信。 原理如下:
首先创建一个文件名为my_fifo的文件,然后进程们以读或写的方式去打开这个文件(以什么方式打开则具有对应的能力)。因为一个文件对应一个inode,所以不同的文件以同样的文件名打开一个文件时,他指向的inode是一样的。所以这个inode就是进程间通信的介质。他指向一块内存用于通信。然后其他的就和匿名管道一样了。
mkfifo test //创建了一个名字为 test 的命名管道
echo "this is a pipe" > test // 写数据
这个时候管道的内容没有被读出的话,那么这个命令就会一直停在这里,只有当另外一个进程把 test 里面的内容读出来的时候这条命令才会结束。
cat < test // 读数据
test 里面的数据被读取出来了。上一条命令也执行结束了。
从上面的例子可以看出,管道的通知机制类似于缓存,就像一个进程把数据放在某个缓存区域,然后等着另外一个进程去拿,并且是管道是单向传输的。
这种通信方式有什么缺点呢?显然,这种通信方式效率低下,你看,a 进程给 b 进程传输数据,只能等待 b 进程取了数据之后 a 进程才能返回。所以管道不适合频繁通信的进程。当然,他也有它的优点,例如比较简单,能够保证我们的数据已经真的被其他进程拿走了。
FIFO进程间通信案例:
/* reader.c */
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
int main() {
int fd;
char buf[1024];
int n;
if (mkfifo("/tmp/myfifo", 0644) < 0) {
perror("mkfifo error:");
exit(1);
}
if ((fd = open("/tmp/myfifo", O_RDONLY)) < 0) {
perror("open fifo error:");
exit(1);
}
if ( ( n = read(fd, buf, sizeof(buf))) < 0) {
perror("read msg error:");
exit(1);
}
buf[n] = '\0';
printf("msg:%s\n", buf);
unlink("/tmp/myfifo");
return 0;
}
/* writer.c */
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>
int main() {
int fd;
const char *msg = "hello from writer";
if ((fd = open("/tmp/myfifo", O_WRONLY)) < 0) {
perror("open fifo error:");
exit(1);
}
if (write(fd, msg, strlen(msg)) < 0) {
perror("write msg error:");
exit(1);
}
return 0;
}
4 消息队列
那我们能不能把进程的数据放在某个内存之后就马上让进程返回呢?无需等待其他进程来取就返回呢?
是可以的,我们可以用消息队列的通信模式来解决这个问题,例如 a 进程要给 b 进程发送消息,只需要把消息放在对应的消息队列里就行了,b 进程需要的时候再去对应的消息队列里取出来。同理,b 进程要给a 进程发送消息也是一样。这种通信方式也类似于缓存吧。
这种通信方式有缺点吗?
答是有的,如果 a 进程发送的数据占的内存比较大,并且两个进程之间的通信特别频繁的话,消息队列模型就不大适合了。因为 a 发送的数据很大的话,意味**发送消息(拷贝)**这个过程需要花很多时间来读内存。
相关系统调用函数:
/*
获取或创建一个消息队列
*/
int msgget(key_t key, int msgflg);
/*
向消息队列中发送消息
msgp: 指向类似下面结构的结构体
struct msgbuf {
long mtype;
char mtext[1];
};
msgsz: 上面结构体的mtext的大小由该字段指定,发送的时候可以使用strlen(buf.mtext), 接收的时候sizeof(buf.mtext)
msgflg: 通常为0
*/
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);
生产者消费者示例:
/* producer.c */
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
typedef struct {
long mtype;
char mtext[1024];
}msgbuf;
int main(int argc, char const *argv[])
{
key_t key;
int msgid;
if ((key = ftok(".", 666)) == -1) {
perror("ftok error:");
return 1;
}
if ((msgid = msgget(key, 0644 | IPC_CREAT)) == -1) {
perror("msgget error:");
return 1;
}
msgbuf buf = {1, "surprise, mother fucker"};
if (msgsnd(msgid, &buf, strlen(buf.mtext), 0) == -1) {
perror("msgsnd error");
return 1;
}
return 0;
}
/* consumer.c */
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
typedef struct {
long mtype;
char mtext[1024];
}msgbuf;
int main(int argc, char const *argv[])
{
key_t key;
int msgid;
if ((key = ftok(".", 666)) == -1) {
perror("ftok error:");
return 1;
}
if ((msgid = msgget(key, 0644 | IPC_CREAT)) == -1) {
perror("msgget error:");
return 1;
}
msgbuf buf;
if (msgrcv(msgid, &buf, sizeof(buf.mtext), 1, 0) == -1) {
perror("msgrcv error");
return 1;
}
printf("recv msg, type:%ld, text:%s\n", buf.mtype, buf.mtext);
msgctl(msgid, IPC_RMID, NULL);
return 0;
}
5 共享内存
共享内存这个通信方式就可以很好着解决拷贝所消耗的时间了。
这个可能有人会问了,每个进程不是有自己的独立内存吗?两个进程怎么就可以共享一块内存了?
我们都知道,系统加载一个进程的时候,分配给进程的内存并不是实际物理内存,而是虚拟内存空间。那么我们可以让两个进程各自拿出一块虚拟地址空间来,然后映射到相同的物理内存中,这样,两个进程虽然有着独立的虚拟内存空间,但有一部分却是映射到相同的物理内存,这就完成了内存共享机制了。
共享内存的原理和消息队列类型,都是开辟一块内存作为通信的介质。
共享内存并未提供锁机制,也就是说,在一个进程对共享内存进行读写时,不会阻止其它进程的读写。共享内存需要借助其它机制来保证进程间的数据同步(比如信号量)。
操作步骤:
1 操作系统有一个全局的结构体数据,每次需要一块共享的内存时(shmget),从里面取一个结构体,记录相关的信息。并返回一个id。
2 调用shmat的时候传入shmget返回的id。shmat根据id找到对应的shmid_ds 结构体。新建一个vm_area_struct结构体。开始地址和结束地址根据shmid_ds 中的信息计算,也就是用户申请的大小。接着把vm_area_struct插入进程中管理vm_area_struct的avl树。并且把一些上下文信息保存到页表项。
3 进程访问共享内存范围中的地址时,触发缺页中断。
4 如果还没分配物理地址则分配,否则直接范围已经分配的地址。如果分配了物理地址,则把物理地址写入进程的页表项。下次就不会缺页中断了。
5 其他进程共享该块内存的时候,如果访问范围内的地址,处理过程是类似的。进程访问某一个地址,发生缺页中断,发现这时候共享内存已经映射了物理地址。最后改写自己的页表项。因为各个进程都对应同一块内存,所以操作的时候会互相感知,实现通信。
使用ipcs命令可以查看共享内存、消息队列和信号量的相关信息:
------ Message Queues --------
key msqid owner perms used-bytes messages
------ Shared Memory Segments --------
key shmid owner perms bytes nattch status
0x00000000 2 liuxi 666 2048 0
------ Semaphore Arrays --------
key semid owner perms nsems
相关系统调用函数:
#include <sys/ipc.h>
#include <sys/shm.h>
/*
创建或获取共享内存
key: 共享内存的键值,是共享内存在系统中的编号,不同共享内存编号不能相同,用十六进制比较好
size: 共享内存的大小,单位byte
shmflg: 共享内存的访问权限,与文件差不多, 0666 | IPC_CREAT 表示所有用户都可读写,IPC_CREAT 表示如果不存在则创建
return val: 成功返回一个id,失败返回-1,并设置errno
*/
int shmget(key_t key, size_t size, int shmflg);
/*
将共享内存链接到当前进程的地址空间
shmid: shmflg返回的id
shmaddr: 共享内存链接到当前进程中的地址位置,通常为NULL,表示让系统来选择
shmflg: 标志位,通常为0
return val: 成功返回共享内存段的地址,失败返回 (void *)-1, 并设置errno
*/
void *shmat(int shmid, const void *shmaddr, int shmflg);
/*
将共享内存从当前进程分离
shmaddr: 共享内存地址
return val: 成功返回0, 失败返回-1, 设置errno
*/
int shmdt(const void *shmaddr);
/*
操作共享内存,可以用来删除一个共享内存
shmid: 共享内存id
cmd: 执行的操作, IPC_RMID表示删除
buf: 设置为NULL
return val: 失败返回-1, 设置errno
*/
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
共享内存使用案例:
一个进程从文件读出内容写入共享内存,另一个进程读共享内存数据,保存入一个新文件。
/* sharemem_r.c */
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#define MAXBUFSIZE 4096
#define SHAREMEMESIZE 4096
int write_file(const char *buf, int size, const char *filename);
int main() {
int shm_id;
char buf[MAXBUFSIZE] = {0};
char *shm_p = NULL;
/*
获取共享内存,如果不存在则创建
*/
if ( (shm_id = shmget(0x666, SHAREMEMESIZE, 0644)) < 0) {
perror("shmget error");
return 1;
}
/*
把共享内存链接到当前进程的地址空间
*/
shm_p = (char *)shmat(shm_id, NULL, 0);
if (shm_p == (void *) -1) {
perror("shmat error:");
return 1;
}
// 拷贝共享内存中的数据
memcpy(buf, shm_p, MAXBUFSIZE);
// 将共享内存中的内容写入write_text文件中
write_file(buf, strlen(buf), "write_text");
printf("write to file: %s\n", buf);
// 把共享内存从当前进程分离
shmdt(shm_p);
//删除共享内存
if (shmctl(shm_id, IPC_RMID, 0) == -1) {
printf("shmctl failed\n");
return -1;
}
return 0;
}
// 将buf中的数据写入文件
int write_file(const char *buf, int size, const char *filename) {
int fd, n;
// 打开文件,不存在则创建
if ((fd = open(filename, O_WRONLY | O_CREAT, 0644)) < 0) {
perror("open file error:");
return -1;
}
// 写入内容
n = write(fd, buf, size);
if (n < 0) {
perror("write file error");
return -1;
}
close(fd);
return n;
}
/* sharemem_w.c */
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>
#include <errno.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#define MAXBUFSIZE 4096
#define SHAREMEMESIZE 4096
int read_file(char *buf, int size, const char *filename);
int main() {
int n, shm_id;
char buf[MAXBUFSIZE] = {0};
char *shm_p = NULL;
/*
获取共享内存,如果不存在则创建
*/
if ( (shm_id = shmget(0x666, SHAREMEMESIZE, 0644 | IPC_CREAT)) < 0) {
perror("shmget error");
return 1;
}
/*
把共享内存链接到当前进程的地址空间
*/
shm_p = (char *)shmat(shm_id, NULL, 0);
if (shm_p == (void *) -1) {
perror("shmat error:");
return 1;
}
// 从文件中读取数据
n = read_file(buf, MAXBUFSIZE, "./read_text");
if (n == -1) {
return 1;
}
printf("read from file: %s\n", buf);
// 将数据拷贝到共享内存
memcpy(shm_p, buf, strlen(buf));
// 把共享内存从当前进程分离
shmdt(shm_p);
return 0;
}
// 读文件内容到buf
int read_file(char *buf, int size, const char *filename) {
int fd, n;
// 打开read_test
if ( (fd = open(filename, O_RDONLY)) < 0) {
perror("open file_read_test error:");
return -1;
}
// 读取文件内容
n = read(fd, buf, size);
if (n < 0) {
perror("read file error:");
return -1;
}
// 关闭文件
close(fd);
return n;
}
6 信号量
共享内存最大的问题是什么?
没错,就是多进程竞争内存的问题,如果多个进程同时修改同一个共享内存,很有可能就冲突了。例如两个进程都同时写一个地址,那先写的那个进程会发现内容被别人覆盖了。
为了防止多进程竞争共享资源,而造成的数据错乱,所以需要保护机制,使得共享的资源,在任意时刻只能被一个进程访问。正好,信号量就实现了这一保护机制。
信号量其实是一个整型的计数器,主要用于实现进程间的互斥与同步,而不是用于缓存进程间通信的数据。
信号量表示资源的数量,控制信号量的方式有两种原子操作:
一个是 P 操作,这个操作会把信号量减去 -1,相减后如果信号量 < 0,则表明资源已被占用,进程需阻塞等待;相减后如果信号量 >= 0,则表明还有资源可使用,进程可正常继续执行。
另一个是 V 操作,这个操作会把信号量加上 1,相加后如果信号量 <= 0,则表明当前有阻塞中的进程,于是会将该进程唤醒运行;相加后如果信号量 > 0,则表明当前没有阻塞中的进程;
P 操作是用在进入共享资源之前,V 操作是用在离开共享资源之后,这两个操作是必须成对出现的。
互斥信号量
接下来举个例子,如果要使得两个进程互斥访问共享内存,我们可以初始化信号量为 1
。
具体的过程如下:
进程 A 在访问共享内存前,先执行了 P 操作,由于信号量的初始值为 1,故在进程 A 执行 P 操作后信号量变为 0,表示共享资源可用,于是进程 A 就可以访问共享内存。
若此时,进程 B 也想访问共享内存,执行了 P 操作,结果信号量变为了 -1,这就意味着临界资源已被占用,因此进程 B 被阻塞。
直到进程 A 访问完共享内存,才会执行 V 操作,使得信号量恢复为 0,接着就会唤醒阻塞中的线程 B,使得进程 B 可以访问共享内存,最后完成共享内存的访问后,执行 V 操作,使信号量恢复到初始值 1。
可以发现,信号初始化为 1
,就代表着是互斥信号量,它可以保证共享内存在任何时刻只有一个进程在访问,这就很好的保护了共享内存。
同步信号量
在多进程里,每个进程并不一定是顺序执行的,它们基本是以各自独立的、不可预知的速度向前推进,但有时候我们又希望多个进程能密切合作,以实现一个共同的任务。
例如,进程 A 是负责生产数据,而进程 B 是负责读取数据,这两个进程是相互合作、相互依赖的,进程 A 必须先生产了数据,进程 B 才能读取到数据,所以执行是有前后顺序的。
那么这时候,就可以用信号量来实现多进程同步的方式,我们可以初始化信号量为 0。
具体过程:
如果进程 B 比进程 A 先执行了,那么执行到 P 操作时,由于信号量初始值为 0,故信号量会变为 -1,表示进程 A 还没生产数据,于是进程 B 就阻塞等待;
接着,当进程 A 生产完数据后,执行了 V 操作,就会使得信号量变为 0,于是就会唤醒阻塞在 P 操作的进程 B;
最后,进程 B 被唤醒后,意味着进程 A 已经生产了数据,于是进程 B 就可以正常读取数据了。
可以发现,信号初始化为 0
,就代表着是同步信号量,它可以保证进程 A 应在进程 B 之前执行。
7 UNIX域套接字
前面提到的信号、管道、消息队列、共享内存、信号量都是在同一台主机上进行进程间通信,那要想跨网络与不同主机上的进程之间通信,就需要 Socket 通信了。
socket API原本是为网络通讯设计的,但后来在socket的框架上发展出一种IPC机制,就是Unix Domanin Socket(UDS)。虽然网络socket也可以用于同一台主机的进程间通讯,但是UDS用于IPC更有效率:不需要经过网络协议栈,不需要打包拆包、计算校验和、维护序号和应答号等,只是将应用层数据从一个进程拷贝到另一个进程。这是因为IPC是可靠的通讯,而网络协议是为不可靠通讯设计的。UDS也提供面向连接和面向数据报两种API接口,类似于TCP与UDP,但是面向数据报的UDS也是可靠的,消息既不会丢失也不会顺序错乱。
UDS是全双工的,API接口语义丰富,相比于其它IPC有明显的优越性,目前已经成为使用最广泛的IPC机制。例如Docker cli与Docker 守护进程的通信就会使用UDS。
使用UDS的步骤与网络socket差不多,也要使用到socket()、bind()、listen()、accept等系统调用。
UDS与网络socket编程最明显的不同在于地址格式不同,用结构体sockaddr_un表示,网络编程的socket地址是IP地址加端口号,而UNIX Domain Socket的地址是一个socket类型的文件在文件系统中的路径,这个socket文件由bind()调用创建,如果调用bind()时该文件已存在,则bind()错误返回。
上述框图大致原理如下。
1 服务器首先拿到一个socket结构体,和一个unix域相关的unix_proto_data结构体。
2 服务器bind一个文件。对于操作系统来说,就是新建一个文件,然后把文件路径信息存在unix_proto_data中。
3 listen
4 客户端通过同样的文件路径调用connect去连接服务器。这时候客户端的结构体插入服务器的连接队列,等待处理。
5 服务器调用accept摘取队列的节点,然后新建一个通信socket进行通信。
unix域通信本质还是基于内存之间的通信,客户端和服务器都维护一块内存,然后实现全双工通信,而unix域的文件路径,只不过是为了让客户端进程可以找到服务端进程。而通过connect和accept让客户端和服务器对应的结构体关联起来,后续就可以互相往对方维护的内存里写东西了。就可以实现进程间通信。
UDS的C/S架构案例:
/* server.c */
#include <stdlib.h>
#include <stdio.h>
#include <stddef.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
int server_listen(const char *name) {
int fd, len, err, ret;
struct sockaddr_un un;
// 创建unix domain socket
if((fd = socket(AF_UNIX, SOCK_STREAM, 0)) < 0) {
perror("socket error");
return -1;
}
// 以防存在,先删除
unlink(name);
memset(&un, 0, sizeof(un));
un.sun_family = AF_UNIX;
strcpy(un.sun_path, name);
len = offsetof(struct sockaddr_un, sun_path) + strlen(name);
// 绑定uds
ret = bind(fd, (struct sockaddr *)&un, len);
if( ret == -1) {
close(fd);
perror("bind error");
return -1;
}
if(listen(fd, 128) < 0) {
close(fd);
perror("listen error");
return -1;
}
return fd;
}
int server_accept(int listenfd, uid_t *uidptr)
{
int clifd, len, err, rval;
time_t staletime;
struct sockaddr_un un;
struct stat statbuf;
len = sizeof(un);
if ((clifd = accept(listenfd, (struct sockaddr *)&un, &len)) < 0) {
return -1;
}
len -= offsetof(struct sockaddr_un, sun_path);
un.sun_path[len] = 0;
if (stat(un.sun_path, &statbuf) < 0) {
close(clifd);
return -1;
}
if (S_ISSOCK(statbuf.st_mode) == 0) {
close(clifd);
return -1;
}
if (uidptr != NULL)
*uidptr = statbuf.st_uid;
unlink(un.sun_path);
return clifd;
}
int main(int argc, char const *argv[])
{
int lfd, cfd, n, i;
uid_t cuid;
char buf[1024];
lfd = server_listen("tmp.sock");
if (lfd < 0) {
exit(-1);
}
while (1) {
cfd = server_accept(lfd, &cuid);
if (cfd < 0) {
exit(-1);
}
while (1) {
n = read(cfd, buf, 1024);
if (n == -1) {
break;
} else if (n == 0) {
printf("the other side has been closed.\n");
break;
}
for (i = 0; i < n; i++) {
buf[i] = toupper(buf[i]);
}
write(cfd, buf, n);
}
}
close(cfd);
close(lfd);
return 0;
}
/* client.c */
#include <stdio.h>
#include <stdlib.h>
#include <stddef.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <errno.h>
#define CLI_PATH "/var/tmp/"
int cli_conn(const char *name)
{
int fd, len, err, rval;
struct sockaddr_un un;
if ((fd = socket(AF_UNIX, SOCK_STREAM, 0)) < 0) {
return -1;
}
memset(&un, 0, sizeof(un));
un.sun_family = AF_UNIX;
sprintf(un.sun_path, "%s%05d", CLI_PATH, getpid());
len = offsetof(struct sockaddr_un, sun_path) + strlen(un.sun_path);
unlink(un.sun_path);
if (bind(fd, (struct sockaddr *)&un, len) < 0) {
perror("bind error");
close(fd);
return -1;
}
/* fill socket address structure with server's address */
memset(&un, 0, sizeof(un));
un.sun_family = AF_UNIX;
strcpy(un.sun_path, name);
len = offsetof(struct sockaddr_un, sun_path) + strlen(name);
if (connect(fd, (struct sockaddr *)&un, len) < 0) {
perror("connect error");
close(fd);
return -1;
}
return fd;
}
int main(void)
{
int fd, n;
char buf[1024];
fd = cli_conn("tmp.sock");
if (fd < 0) {
exit(-1);
}
while (fgets(buf, sizeof(buf), stdin) != NULL) {
write(fd, buf, strlen(buf));
n = read(fd, buf, sizeof(buf));
write(STDOUT_FILENO, buf, n);
}
close(fd);
return 0;
}
8 mmap
存储映射I/O (Memory-mapped I/O) 使一个磁盘文件与存储空间中的一个缓冲区相映射。于是当从缓冲区中取数据,就相当于读文件中的相应字节。类似,将数据存入缓冲区,则相应的字节就自动写入文件。这样,就可在不适用read和write函数的情况下,使用地址(指针)完成I/O操作。
使用这种方法,首先应通知内核,将一个指定文件映射到存储区域中。这个映射工作可以通过mmap函数来实现。
mmap可以映射文件,从而达到进程间通信的目前,mmap的原理:
1 打开一个文件,拿到一个文件描述符。
2 根据mmap的参数,申请一个vma结构体。并且传入fd表示映射文件。
3 把vma插入到调用进程的vma链表和树中。返回首地址(用户指定或者系统默认分配)。
4 用户通过3中返回的地址,进行内存的读写,这时候对应的是文件的读写。
5 另一个进程同样执行1-4的步骤,即有两个进程都映射到同一个文件。两个进程进行读写的时候,就完成了进程间通信。
总结
由于每个进程的用户空间都是独立的,不能相互访问,这时就需要借助内核空间来实现进程间通信,原因很简单,每个进程都是共享一个内核空间。
Linux 内核提供了不少进程间通信的方式,其中最简单的方式就是管道,管道分为「匿名管道」和「命名管道」。
匿名管道顾名思义,它没有名字标识,匿名管道是特殊文件只存在于内存,没有存在于文件系统中,shell 命令中的「|」竖线就是匿名管道,通信的数据是无格式的流并且大小受限,通信的方式是单向的,数据只能在一个方向上流动,如果要双向通信,需要创建两个管道,再来匿名管道是只能用于存在父子关系的进程间通信,匿名管道的生命周期随着进程创建而建立,随着进程终止而消失。
命名管道突破了匿名管道只能在亲缘关系进程间的通信限制,因为使用命名管道的前提,需要在文件系统创建一个类型为 p 的设备文件,那么毫无关系的进程就可以通过这个设备文件进行通信。另外,不管是匿名管道还是命名管道,进程写入的数据都是缓存在内核中,另一个进程读取数据时候自然也是从内核中获取,同时通信数据都遵循先进先出原则,不支持 lseek 之类的文件定位操作。
消息队列克服了管道通信的数据是无格式的字节流的问题,消息队列实际上是保存在内核的「消息链表」,消息队列的消息体是可以用户自定义的数据类型,发送数据时,会被分成一个一个独立的消息体,当然接收数据时,也要与发送方发送的消息体的数据类型保持一致,这样才能保证读取的数据是正确的。消息队列通信的速度不是最及时的,毕竟每次数据的写入和读取都需要经过用户态与内核态之间的拷贝过程。
共享内存可以解决消息队列通信中用户态与内核态之间数据拷贝过程带来的开销,它直接分配一个共享空间,每个进程都可以直接访问,就像访问进程自己的空间一样快捷方便,不需要陷入内核态或者系统调用,大大提高了通信的速度,享有最快的进程间通信方式之名。但是便捷高效的共享内存通信,带来新的问题,多进程竞争同个共享资源会造成数据的错乱。
那么,就需要信号量来保护共享资源,以确保任何时刻只能有一个进程访问共享资源,这种方式就是互斥访问。信号量不仅可以实现访问的互斥性,还可以实现进程间的同步,信号量其实是一个计数器,表示的是资源个数,其值可以通过两个原子操作来控制,分别是 P 操作和 V 操作。
与信号量名字很相似的叫信号,它俩名字虽然相似,但功能一点儿都不一样。信号是进程间通信机制中唯一的异步通信机制,信号可以在应用进程和内核之间直接交互,内核也可以利用信号来通知用户空间的进程发生了哪些系统事件,信号事件的来源主要有硬件来源(如键盘 Cltr+C )和软件来源(如 kill 命令),一旦有信号发生,进程有三种方式响应信号 1. 执行默认操作、2. 捕捉信号、3. 忽略信号。有两个信号是应用进程无法捕捉和忽略的,即 SIGKILL 和 SEGSTOP,这是为了方便我们能在任何时候结束或停止某个进程。
前面说到的通信机制,都是工作于同一台主机,如果要与不同主机的进程间通信,那么就需要 Socket 通信了。Socket 实际上不仅用于不同的主机进程间通信,还可以用于本地主机进程间通信,可根据创建 Socket 的类型不同,分为三种常见的通信方式,一个是基于 TCP 协议的通信方式,一个是基于 UDP 协议的通信方式,一个是本地进程间通信方式。