进程通信(1.0 单机进程通信)
进程通信(1.0 单机进程通信)
参考链接:
https://blog.csdn.net/qq_38954792/article/details/107685163
http://blog.sina.com.cn/s/blog_6a1837e90100v1vc.html
https://www.cnblogs.com/xiaoshiwang/p/9813815.html
https://www.cnblogs.com/leeming0222/articles/3994125.html
https://www.cnblogs.com/hongbo-tao/p/13344333.html
关键词:
LINUX、C语言、进程通信、信号
操作系统中,每个进程的地址相互独立,进程通信必须经过内核。、
本文主要介绍单机环境下的进程通信方式。
1. 进程通信的方式
- 文件
- 管道
- 内存映射
- 共享内存
- 信号
- 套接字
- 消息队列
- 剪切板
- 远程过程调用
- ... ...
2. 单机环境中常见的进程通信方式
2.1 管道通信(也称为匿名管道)
用于有血缘关系的进程间通信。
特点:
- 使用两个文件描述符,一端表示读,一端表示写
- 两个进程都终结管道才终结
- 读端以及写端默认是阻塞的
- 本质是一个内核缓冲区,使用循环队列实现
- 不可反复读取
函数原型:
/**
* 创建单向通信通道(管道)。
* 如果成功,则在管道中存储两个文件描述符;
* 写入管道[1]的字节可以从管道[0]读取。
* 参数:
* 1. 用来接收文件文件描述符的数组
* 返回值:
* 1. 执行结果,如果成功,则返回0;如果不成功,则返回1
**/
int pipe(int [2]);
2.2 命名管道
命名管道(FIFO)可以进行没有关联的进程间通信。
2.2.1 基本概念
特点:
- FIFO是Unix/Linux基础文件类型的一种,文件类型用
p
标识。 - FIFO在文件磁盘上数据块大小为0,仅用来标识内核中的通道。
- 管道文件严格遵循先进先出的方式,读取从开始处返回数据,写入将数据添加到末尾。
- writer()向管道中写入数据,read()从管道中读出数据,当read(),管道中的数据不再存在。
- 以open(path, O_RDONLY)的方式打开管道,只有第一次read()才是阻塞的,如果要每次read()都阻塞必须read()后关闭再open。
- FIFO不能用O_RDWR方式打开。
函数原型:
/**
* 创建FIFO文件
* 参数:
* 1. 文件名
* 2. 权限
* 返回值:
* 1. 文件描述符
*/
int mkfifo (const char *__path, __mode_t __mode);
// 打开文件
int open(const char * pathname, int flags);
// 读文件
int read(int fd,void *buf,int len);
// 写文件
int write(int fd,void *buf,int len);
// 关闭文件
int close(const char * pathname);
// 删除文件
int unlink(const char *__path);
2.2.2 示例
Client:
// 以写阻塞方式打开文件(同时只有一个进程写)
wClientFd = open(FIFO_PATH, O_WRONLY);
// 写入文件
write(wClientFd, &stArgs, sizeof(LaArgs));
// 关闭文件
close(wClientFd);
Server:
// 创建fifo文件
wLaFIFO = mkfifo(FIFO_PATH, 0777);
// 以读阻塞方式(没读到数据会被阻塞),打开fifo文件
wServerFd = open(FIFO_PATH, O_RDONLY);
// 从FIFO中读取数据
read(wServerFd, &stArgs, sizeof(LaArgs));
// 关闭FIFO文件
close(wServerFd);
// 退出server时,删除FIFO文件
unlink(FIFO_PATH);
2.3 内存映射
存储映射是使磁盘中的文件与内存中的缓冲区相映射。
在缓冲区中读取数据,相当于读取文件中的数据;将数据写入缓冲区,相当于写入文件。
特点:
- 将特殊文件进行匿名内存映射,可以为关联进程提供共享内存空间;
- 为无关联的进程提供共享内存空间,一般也是将一个普通文件映射到内存中。
- 对需要进行频繁读写的文件进行使用使用,用内存读写取代I/O读写,以获得较高的性能;
工作流程:
1. FILE *fopen( const char * filename, const char * mode ); //打开一个文件,获取文件描述符
2. void *mmap (void *__addr, size_t __len, int __prot, int __flags, int __fd, __off_t __offset); //建立内存映射
3. //指针操作内存
4. int munmap(void *__addr, size_t __len); int fclose( FILE *fp );//释放资源
函数原型:
/**
* 参数:
* 1:映射起始地址,一般设置NULL,由操作系统指定
* 2:映射长度
* 3:读写模式
* PROT_EXEC 映射区域可被执行
* PROT_READ 映射区域可被读取
* PROT_WRITE 映射区域可被写入
* PROT_NONE 映射区域不能存取
* 4:写入映射区是否写入到文件中
* MAP_FIXED 如果参数start所指的地址无法成功建立映射时,则放弃映射,不对地址做修正。通常不鼓励用此旗标。
* MAP_SHARED 对映射区域的写入数据会复制回文件内,而且允许其他映射该文件的进程共享。
* MAP_PRIVATE 对映射区域的写入操作会产生一个映射文件的复制,即私人的“写入时复制”(copy on write)对此区域作的任何修改都不会写回原来的文件内容。
* MAP_ANONYMOUS 建立匿名映射。此时会忽略参数fd,不涉及文件,而且映射区域无法和其他进程共享。
* MAP_DENYWRITE 只允许对映射区域的写入操作,其他对文件直接写入的操作将会被拒绝。
* MAP_LOCKED 将映射区域锁定住,这表示该区域不会被置换(swap)。
* 5:fopen打开的文件描述符(当使用MAP_ANONYMOUS时,__fd取 -1)
* 6:文件偏移长度,通常设置为0,代表从文件最前方开始对应,offset必须是分页大小的整数倍。
* 返回:
* 1. 若映射成功则返回映射区的内存起始地址,否则返回MAP_FAILED(-1),错误原因存于errno中。
* 错误代码:
* EBADF 参数fd 不是有效的文件描述词
* EACCES 存取权限有误。如果是MAP_PRIVATE 情况下文件必须可读,使用MAP_SHARED则要有PROT_WRITE以及该文件要能写入。
* EINVAL 参数start、length 或offset有一个不合法。
* EAGAIN 文件被锁住,或是有太多内存被锁住。
* ENOMEM 内存不足。
*/
void *mmap (void *__addr, size_t __len, int __prot, int __flags, int __fd, __off_t __offset)
2.4 信号
信号是信息的载体。当一个进程收到信号后,暂停运行,去执行信号处理函数,类似于中断,所以也称作(软中断)。
2.4.1 基本概念
软中断信号,用于通知进程发生了异步事件。进程之间可以互相通过系统调用kill发送软中断信号。通过signal
设置信号处理方法
信号只是用来通知某进程发生了什么事件,并不给该进程传递任何数据。
2.4.2 信号处理方法
进程通过系统调用signal来指定进程对某个信号的处理行为。
- 第一种是类似中断的处理程序,对于需要处理的信号,进程可以指定处理函数,由该函数来处理。
- 第二种方法是,忽略某个信号,对该信号不做任何处理。
- 第三种方法是,对该信号的处理保留系统的默认值,这种缺省操作,对大部分的信号的缺省操作是使得进程终止。
在进程表的表项中有一个软中断信号域,该域中每一位对应一种信号,当有信号发送给进程时,对应位置位。
由此可以看出,进程对不同的信号可以同时保留,但对于同一个信号,进程并不知道在处理之前来过多少个。
2.4.3 信号的类型
实际含义:signum.h
列表: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
2.4.4 系统调用--设置信号处理方法
方式一:
void (signal(int signum, void (handler)(int)))(int);
/**
* signal说明
* 参数:
* 1. signum:指出要设置处理方法的信号
* 2. handler:
* 处理函数
* SIG_IGN:忽略参数signum所指的信号。
* SIG_DFL:恢复参数signum所指信号的处理方法为默认值。
* 返回:
* 1. 成功执行时,返回0。失败返回-1,errno被设为以下的某个值 EINVAL:指定的信号码无效(参数 sig 不合法) EPERM;权限不够无法传送信号给指定进程 ESRCH:参数 pid 所
* 指定的进程或进程组不存在
**/
void (*signal(int signum, void (*handler)(int)))(int);
方式二:
int sigaction(int, const struct sigaction * __restrict, struct sigaction * __restrict);
(POSIX的定义,不通用)不建议使用,如果要了解自己百度。
2.4.5 系统调用--设置信号发送方法
/**
* kill说明
* 参数:
* 1. pid:可能选择有以下四种
* pid大于零时,pid是信号欲送往的进程的标识。
* pid等于零时,信号将送往所有与调用kill()的那个进程属同一个使用组的进程。
* pid等于-1时,信号将送往所有调用进程有权给其发送信号的进程,除了进程1(init)。
* pid小于-1时,信号将送往以-pid为组标识的进程。
* 2. sig:准备发送的信号代码,假如其值为零则没有任何信号送出,但是系统会执行错误检查,通常会利用sig = 0来检验某个进程是否仍在执行。
* 返回:
* 1. 成功执行时,返回0。失败返回-1,errno被设为以下的某个值 EINVAL:指定的信号码无效(参数 sig 不合法) EPERM;权限不够无法传送信号给指定进程 ESRCH:参数 pid 所
* 指定的进程或进程组不存在
**/
int kill(pid_t pid, int sig);
TIPS:
- 获取进程号(PID),C语言通过POPEN执行shell命令:
ps -e | grep 'test' | awk 'print $1'
2.4.6 示例
客户端 lac:
#include
#include
#include
#include
int GetPID()
{
FILE *pp = popen("ps -e | grep 'las' | awk -F' ' '{print $1;}'", "r"); //建立管道
if (!pp) {
printf("建立管道失败\n");
return -1;
}
char tmp[1024];
fgets(tmp, sizeof(tmp), pp);
pclose(pp);
return atoi(tmp);
}
int main()
{
printf("process id is %d \n",GetPID());
printf("return %d\n", kill(GetPID(), SIGHUP));
return 0;
}
服务端 las:
#include
#include
#include
void sigroutine(int dunno)
{
printf("RECEIVE SIG: %d\n", dunno);
switch (dunno)
{
case 1:
printf("Get a signal -- SIGHUP \n");
break;
case 2:
printf("Get a signal -- SIGINT \n");
break;
}
}
int main()
{
signal(SIGHUP, sigroutine);
signal(SIGINT, sigroutine);
while (1)
{
sleep(300);
}
}
2.5 共享内存
共享内存的实质是将内核的一块内存映射到进程中,操作本地的内存相当与操作共享内存,用于无关联的进程之间。
2.5.1 Posix共享内存区
Posix提供的两种在无亲缘关系的进程间共享内存区的方法:
- 内存映射文件:由open()打开某个文件,再由mmap()将得到的描述符映射到当前进程地址空间中
的一个文件。 - 共享内存区对象:有shm_open()打开一个Posix IPC名字(可能是文件系统中的一个路径名),所返
回的描述符有mmap()映射到当前进程的地址空间。
这里只对shm_open()方式进行介绍。
工作流程:
1. int shm_open(const char *name, int oflag, mode_t mode); //创建或打开共享内存, 返回文件描述符
2. int ftruncate(int fd, off_t FILE_SIZE); //设置文件大小
3. void* mmap( void * addr , size_t len , int prot , int flags , int fd , off_t offset ) //将文件映射到内存,返回指向首地址的指针
4. int munmap(void *start, size_t length); //解除地址映射
5. int shm_unlink(const char *name); //删除shm_open()创建的共享内存
函数原型:
/**
* 函数参数:
* 1:const char *name 共享内存文件名,打开位置位于/dev/shm目录,name不能带路径,一般一'/'为首字母,例如“/shmLaArgs”
* 2:int oflag O_CREAT(不存在则创建,存在则当这个标识不存在)、O_RDWR(读写打开)、O_EXCL(配合O_CREAT使用,当存在时,返回错误)
* 3:mode_t mode 权限模式,常见的0644、0755、0777等
* 返回值:
* 成功:返回文件描述符fd
* 失败:返回-1
*/
int shm_open(const char *name, int oflag, mode_t mode);
/**
* 函数参数:
* 1. int fd 文件描述符,所有用open打开的文件描述符都可以使用,这里用来控制shm_open打开的文件描述符
* 2. off_t FILE_SIZE 设置文件大小
* 返回值:
* 成功:返回0
* 失败:返回-1
*/
int ftruncate(int fd, off_t FILE_SIZE); //调整共享内存空间大小
/**
* 函数参数:
* 1. void * addr 指定映射的目标内存首地址, 一般设为NULL就好了,让操作系统自行决定
* 2. size_t len 映射内存的长度,大小为页大小的整数倍(页大小一般是4KB), 不足一页会被补齐为一页。
* 3. int prot 内存保护表示, PROT_EXEC, 页内容可以被执行
* PROT_READ, 页内容可以被读取
* PROT_WRITE,页可以被写入
* PROT_NONE, 页不可访问
* 4. int flags 指定映射对象的类型,映射选项和映射页是否可以共享。它的值可以是一个或者多个以下位的组合体(一般只使用MAP_SHARED就好了)
* MAP_SHARED, 与其它所有映射这个对象的进程共享映射空间。对共享区的写入,相当于输出到文件。直到msync()或者munmap()被调用,才进行更新。
* MAP_FIXED, 使用指定的映射起始地址,如果由start和len参数指定的内存区重叠于现存的映射空间,重叠部分将会被丢弃。如果指定的起始地址不可用,操作将会失败。并且起始地址必须落在页的边界上。
* MAP_PRIVATE, 建立一个写入时拷贝的私有映射。内存区域的写入不会影响到原文件。
* MAP_DENYWRITE, 这个标志被忽略。
* MAP_EXECUTABLE, 这个标志被忽略。
* MAP_NORESERVE, 不为这个映射保留交换空间。(内存不足时,可能会引起段错误)
* MAP_LOCKED, 锁定映射区的页面,从而防止页面被交换出内存。
* MAP_GROWSDOWN, 用于堆栈,告诉内核VM系统,映射区可以向下扩展。
* MAP_ANONYMOUS, 匿名映射,映射区不与任何文件关联。
* MAP_ANON, MAP_ANONYMOUS的别称,不再被使用
* MAP_FILE, 兼容标志,被忽略。
* MAP_32BIT, 将映射区放在进程地址空间的低2GB,MAP_FIXED指定时会被忽略。当前这个标志只在x86-64平台上得到支持。
* MAP_POPULATE, 为文件映射通过预读的方式准备好页表。随后对映射区的访问不会被页违例阻塞。
* MAP_NONBLOCK, 仅和MAP_POPULATE一起使用时才有意义。不执行预读,只为已存在于内存中的页面建立页表入口。
* 5. int fd 打开的文件描述符,如果指定了MAP_ANONYMOUS,则该值应设为`-1`
* 6. off_t offset 被映射对象内容的起点,文件起点(一般设置为0就好了)
* 返回值:
* 成功:返回指向映射结果内存的首地址(void *)
* 失败:返回(void *)-1
*/
void* mmap(void * addr, size_t len, int prot, int flags, int fd, off_t offset)//将文件映射到内存,返回指向首地址的指针,操作的是内存,操作系统将脏数据写回
/**
* 函数参数:
* 1:void *start mmap映射的内存首地址
* 2:size_t length 映射的内存长度
* 返回值:
* 成功:返回0
* 失败:返回-1
*/
int munmap(void *start, size_t length); //解除地址映射
/**
* 函数参数:
* 1:const char *name shm_open打开的文件名
* 返回值:
* 成功:返回0
* 失败:返回-1
*/
int shm_unlink(const char *name); //删除shm_open()创建的共享内存
2.5.2 System V共享内存区
函数原型:
/**
* 函数参数:
* 1:共享内存唯一key
* 2:共享内存大小
* 3:创建模式以及权限
* 返回值:
* 成功:返回共享内存ID(与key不一定相同)
* 失败:返回-1
*/
int shmget(key_t key, size_t size, int shmflg);
/**
* 函数参数:
* 1:共享内存ID
* 2:设置当前进程中的内存地址(与共享内存地址不一定相同),一般为NULL
* 3:读写模式
* 返回值:
* 成功:当前内存地址
* 失败:返回-1(void *)
*/
void *shmat(int, const void *, int);
/**
* 函数参数:
* 当前进程相关联的内存地址
* 返回值:
* 成功:返回0
* 失败:返回-1
*/
int shmdt(const void *);
/**
* 函数参数:
* 1:共享内存ID
* 2:操作(获取状态信息、设置信息、删除)
* 3:内容
* 返回值:
* 成功:返回0
* 失败:返回-1
*/
int shmctl(int, int, struct shmid_ds *)
2.5.3 示例
client:
// Client
//打开共享内存
wShm = shm_open(SHM_ARGS, (O_CREAT | O_RDWR), 0644);
if (wShm == -1)
{
LaLogError("Failed to open shared memory\n");
return 0;
}
//设置文件大小
ftruncate(wShm, sizeof(LaArgs));
//创建映射
pstArgs = mmap(NULL, sizeof(LaArgs), PROT_READ | PROT_WRITE, MAP_SHARED, wShm, 0);
//利用信号量实现互斥
pstRead = sem_open(SEM_READ, O_RDWR);
pstWrite = sem_open(SEM_WRITE, O_RDWR);
pstMutex = sem_open(SEM_MUTEX, O_RDWR);
sem_wait(pstWrite);
sem_wait(pstMutex);
//向共享内存写入数据
pstArgs->eRID = stArgs.eRID;
pstArgs->wDays = stArgs.wDays;
pstArgs->wType = stArgs.wType;
sprintf(pstArgs->szConference, "%s", stArgs.szConference);
sprintf(pstArgs->szEndTime, "%s", stArgs.szEndTime);
sprintf(pstArgs->szStartTime, "%s", stArgs.szStartTime);
sem_post(pstRead);
sem_post(pstMutex);
sem_close(pstRead);
sem_close(pstWrite);
sem_close(pstMutex);
//关闭映射
munmap(pstArgs, sizeof(LaArgs));
server:
// Server
// 创建共享内存
wShm = shm_open(SHM_ARGS, (O_CREAT | O_RDWR), 0644);
if (wShm == -1)
{
LaLogError("Failed to create shared memory\n");
return 0;
}
ftruncate(wShm, sizeof(LaArgs));
pstArgs = mmap(NULL, sizeof(LaArgs), PROT_READ | PROT_WRITE, MAP_SHARED, wShm, 0);
// 创建信号量
pstRead = sem_open(SEM_READ, (O_CREAT | O_RDWR), 0644, 0);
pstWrite = sem_open(SEM_WRITE, (O_CREAT | O_RDWR), 0644, 1);
pstMutex = sem_open(SEM_MUTEX, (O_CREAT | O_RDWR), 0644, 1);
// 阻塞处理
while (1)
{
sem_wait(pstRead);
sem_wait(pstMutex);
printf("pstArgs->wType: %d\n", pstArgs->wType);
if (pstArgs->wType == LA_TYPE_KILL)
{
ExecHandle(pstArgs);
break;
}
dBeginTime = clock();
// 线程执行
if (pthread_create(&stThId, NULL, (void *)LaMain, pstArgs) != 0)
{
LaLogError("thread creation failed\n");
ExecHandle(pstArgs);
exit(1);
}
// 线程同步控制
while (1)
{
if (pstArgs->wStatus == 0)
{
break;
}
usleep(300);
wCount += 1;
if (wCount > 1000)
{
LaLogDebug("Thread timeout\n");
ExecHandle(pstArgs);
exit(1);
}
}
dEndTime = clock();
UcLogDebug("Running Times: %lf\n", (dEndTime - dBeginTime)/CLOCKS_PER_SEC);
sem_post(pstWrite);
sem_post(pstMutex);
}