什么是进程间通信,就是进程与进程之间进行通信,互相发送消息;可以通过 信号 或者 管道 或者 消息队列 或者 信号量 去通信!
目录
一、信号
1. 信号简介
2. 都有那些信号?
3. 注册信号的函数
1). signal
2). sigaction (项目中强烈推荐使用)
4. 信号发送
1). kill 函数
2). alarm 函数
3). raise 函数
5. 发送多个信号的情况
6. 信号集
1). sigemptyset
2). sigfillset
3). sigdelset
4). sigaddset
5). sigismember
7. 进程的“信号屏蔽字”
1). 修改进程的“信号屏蔽字”
2). 获取未处理的信号
8. 阻塞式等待信号
1). pause
2). sigsuspend
二、管道
1. 管道简介
2. 管道的创建
1). pipe
2). 例一 :多进程使用管道通信
3). 例二:关闭管道的读端/写端
4). 例三:父进程循环给子进程发送消息
3. popen / pclose
1). popen
2). pclose
3). 例一:读取命令返回的数据
4). 例二:把输出写到外部程序
5). popen的优缺点
三、消息队列
1. 什么是消息队列?
2. msgget 消息队列的获取
3. msgsnd 消息的发送
4. msgrcv 消息的接收
5. msgctl 消息的控制
6. 示例一:两程序间通过消息队列去通信
7. 多进程间消息队列通信
8. 练习
四、信号量
1. 什么是信号量
2. semget 信号量的获取
3. semop 信号量的操作
4. semctl 信号量的控制
5. 例
五、共享内存机制
六、总结
一、信号
1. 信号简介
什么是信号?
信号是给程序提供一种可以处理异步事件的方法,它利用软件中断来实现。不能自定义信号,所有信号都是系统预定义的。
信号由谁产生?
1. 由shell终端根据当前发生的错误(段错误、非法指令等)Ctrl+c而产生相应的信号
比如:
socket通信或者管道通信,如果读端都已经关闭,执行写操作(或者发送数据),
将导致执行写操作的进程收到SIGPIPE信号(表示管道破裂);
该信号的默认行为:终止该进程。
2. 在shell终端,使用kill或killall命令产生信号
例:注册SIGINT信号,也就是键盘按下ctrl+c后触发的信号,让其执行我们代码中自己定义的函数!
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <errno.h>
// 自定义信号处理函数,其有固定格式,返回值:void; 参数 int
void myHandle(int sig) {
printf("catch a signal : %d\n", sig);
}
int main(int argc, char **argv) {
// 注册SIGINT信号(ctrl+c),接收到此信号执行myHandle方法
sighandler_t result = signal(SIGINT, myHandle);
if (SIG_ERR == result) {
printf("发生错误了!\n");
perror("错误原因:");
exit(-1);
}
while (1) sleep(1);
return 0;
}
当然,也可以在另一个终端使用kill命令去发送信号: kill -SIGINT 进程id
2. 都有那些信号?
信号名称 | 说明 |
---|---|
SIGABORT | 进程异常终止 |
SIGALRM | 超时警告 |
SIGFPE | 浮点运算异常 |
SIGHUP | 连接挂断 |
SIGILL | 非法指令 |
SIGINT | 终端中断(Ctrl+C 将产生该信号) |
SIGKILL | 终止进程 |
SIGPIPE | 向没有读权限的进程的管道写数据 |
SIGQUIT | 终端退出(Ctrl+\ 将产生该信号) |
SIGSEGV | 无效内存段访问 |
SIGTERM | 终止 |
SIGUSR1 | 用户自定义信号1 |
SIGUSR2 | 用户自定义信号2 |
---华丽的分割线--- | 在此以上的信号如果不被捕获,则进程自己接收到后都会终止; |
SIGCHLD | 子进程已停止或退出 |
SIGCONT | 让暂停的进程继续执行 |
SIGSTOP | 停止执行(即“暂停” |
SIGTTIN | 后台进程尝试读操作 |
SIGTTOU | 后台进程尝试写操作 |
信号的处理
1. 忽略此信号,signal(SIGINT, SIG_IGN); // 忽略SIGINT(ctrl+c)信号
2. 捕捉信号,指定的信号处理函数进行处理,向上面👆代码那样,注册了之后,会自动捕获!
3. 执行系统默认动作,即不用捕捉它,让系统自己处理,大多数都是终止进程的信号;
信号的捕获
信号的捕获,是指,接收到某种信号后,去执行指定的函数;
注意:SIGKILL 和 SIGSTOP 不能被捕获,即这两种信号的响应动作不能被改变。
3. 注册信号的函数
1). signal
#include <signal.h>
typedef void (*sighandler_t)(int); // 信号处理函数定义格式
sighandler_t signal(int signum, sighandler_t handler);
描述:注册信号,使得收到对应注册的信号后执行指定的函数;
参数:
signum
信号;
handler
函数指针;
还可以使用以下特殊值:
SIG_IGN 忽略信号
SIG_DFL 恢复默认行为
返回值:
成功:返回上一个信号处理函数的指针,如果是第一次执行signal,则返回SIG_DFL;
失败:返回SIG_ERR,并设置错误标志errno。
例:
注册SIGINT信号,使得其执行代码中自定义的函数,然后再函数中恢复默认行为
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <errno.h>
void myHandle(int sig) {
printf("catch a signal : %d\n", sig);
// 恢复默认函数行为
signal(SIGINT, SIG_DFL); // 等同于signal(sig, SIG_DFL);
}
int main(int argc, char **argv) {
// 注册SIGINT信号(ctrl+c),接收到此信号执行myHandle方法
sighandler_t result = signal(SIGINT, myHandle);
if (SIG_ERR == result) {
printf("发生错误了!\n");
perror("错误原因:");
exit(-1);
}
while (1) sleep(1);
return 0;
}
第一次按下ctrl+c,执行了自定义的函数;第二次再按ctrl+c ,程序就结束了!达到预期效果!
另外,SIGUSR1 和 SIGUSR2是可以给我们程序员自己使用的,他俩没有默认绑定任何信号操作函数,可以根据自己的实际需求去注册绑定和使用!
例:
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
void myHandle1(int sig) {
printf("catch a signal : %d\n", sig);
}
void myHandle2(int sig) {
printf("接收到了信号 : %d\n", sig);
}
int main(int argc, char **argv) {
// 注册SIGUSR1和SIGUSR2信号,接收到此信号执行myHandle方法
signal(SIGUSR1, myHandle1);
signal(SIGUSR2, myHandle2);
while (1) sleep(1);
return 0;
}
2). sigaction (项目中强烈推荐使用)
sigaction与signal的区别: sigaction比signal更“健壮”,建议使用sigaction;
因为signal是前期很早之前的函数了,会有很多欠缺的地方;所以sigaction横空出世!
#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
描述:sigaction()系统调用用于改变进程在上采取的操作接收特定信号。
参数:
sigumn
信号;可以是除SIGKILL和SIGSTOP之外的任何有效信号;
act
结构体;一些对信号的设置;
oldact
结构体;获取上一个的信号设置act;
返回值:
成功:返回0;
失败:返回-1,并设置错误标志errno;
结构 struct sigaction
struct sigaction {
void (*sa_handler)(int); /* 信号相应的函数 */
void (*sa_sigaction)(int, siginfo_t *, void *); // 可以不管它
sigset_t sa_mask; /* 屏蔽信号集 */
int sa_flags; /* 置为0即可,其他操作可以 man 2 sigaction 去查看 */
void (*sa_restorer)(void); // 可以不管他
};
屏蔽信号集
并不是说屏蔽该信号;如果sa_mask包含了信号A,在信号处理函数期间,信号A触发了,则阻塞信号A,直到信号处理函数结束,才开始处理信号A;即,信号处理函数执行完之后,再响应该信号A。(下面 多信号发送 会有例子)
例:
使用sigaction注册信号,且sa_flags设置为SA_RESETHAND,即只会调用一次指定的处理函数,之后恢复默认行为
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <errno.h>
void myHandle(int sig) {
printf("catch a signal : %d\n", sig);
}
int main(int argc, char **argv) {
struct sigaction act;
struct sigaction oldact;
act.sa_handler = myHandle; // 处理函数
sigemptyset(&act.sa_mask); // 设置为0
//act.sa_flags = 0;
act.sa_flags = SA_RESETHAND; // 只会调用一次上面指定的处理函数,之后恢复默认行为
// 注册SIGINT信号(ctrl+c),接收到此信息执行myHandle方法
int ret = sigaction(SIGINT, &act, &oldact);
if (-1 == ret) {
printf("sigaction error!\n");
perror("reason:");
exit(-1);
}
while (1) sleep(1);
return 0;
}
第一次按下ctrl+c,执行了自定义的函数;第二次再按ctrl+c ,程序就结束了!达到预期效果!
4. 信号发送
在前面的例子中,使用kill可以在终端给进程发送信号;当然,kill也有函数,也可以使用函数去发送信号;
信号的发送方式:
- 1. 在shell终端用快捷键产生信号;
- 2. 使用kill,killall命令;
- 3. 使用kill函数和alarm函数和raise函数。
- 2. 使用kill,killall命令;
1). kill 函数
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
描述:给指定的进程发送指定的信号;
参数:
pid
进程id;
sig
信号;
返回值:
成功:返回0;
失败:返回-1,并设置错误标志errno;
注意:
给指定的进程发送信号需要权限:即普通用户只能给普通用户的进程发送信号,root用户可以给所有用户的进程发送信号;
例1:
创建一个子进程,子进程每秒中输出字符串“child process work!",父进程等待用户输入,如果用户按下字符A, 则向子进程发信号SIGUSR1, 子进程的输出字符串改为大写; 如果用户按下字符a, 则向子进程发信号SIGUSR2, 子进程的输出字符串改为小写
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <errno.h>
int workflag = 0;
void work_up_handle(int sig) {
workflag = 1;
}
void work_down_handle(int sig) {
workflag = 0;
}
int main(int argc, char **argv) {
pid_t pd;
char c;
// 创建一个子进程
pd = fork();
if (-1 == pd) {
printf("fork error!\n");
exit(1);
} else if (0 == pd) {
char *msg;
struct sigaction act;
act.sa_flags = 0;
act.sa_handler = work_up_handle;
sigemptyset(&act.sa_mask);
int ret = sigaction(SIGUSR1, &act, 0); // 注册SIGUSR1信号
if (-1 == ret) {
printf("sigaction SIGUSR1 error!\n");
perror("reason:");
exit(-1);
}
act.sa_handler = work_down_handle;
ret = sigaction(SIGUSR2, &act, 0); // 注册SIGUSR2信号
if (-1 == ret) {
printf("sigaction SIGUSR2 error!\n");
perror("reason:");
exit(-2);
}
while (1) {
if (!workflag) {
msg = "child process work!";
} else {
msg = "CHILD PROCESS WORK!";
}
printf("%s\n", msg);
sleep(2);
}
} else {
while(1) {
c = getchar();
if ('A' == c) {
// 给子进程发送SIGUSR1信号
int ret = kill(pd, SIGUSR1);
if (-1 == ret) {
printf("kill SIGUSR1 error!\n");
perror("reason:");
}
} else if ('a' == c) {
// 给子进程发送SIGUSR2信号
int ret = kill(pd, SIGUSR2);
if (-1 == ret) {
printf("kill SIGUSR2 error!\n");
perror("reason:");
}
}
}
}
return 0;
}
例2:
父进程创建子进程后,注册一个信号后旧调用函数pause()挂起(休眠),直到父进程收到任意一个信号才会唤醒;子进程五秒后给父进程发送任意一个信号,唤醒父进程,使其结束程序!
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <errno.h>
int workflag = 0;
void wake_handle(int sig) {
workflag = 1;
}
int main(int argc, char **argv) {
pid_t pd;
pd = fork(); // 创建进程
if (-1 == pd) {
printf("fork error!\n");
exit(-1);
} else if (0 == pd) {
sleep(5);
int ret = kill(getppid(), SIGALRM); // 给父进程发送SIGALRM信号
if (-1 == ret) {
printf("kill SIGALRM error!\n");
perror("reason:");
}
} else {
struct sigaction act;
act.sa_handler = wake_handle;
act.sa_flags = 0;
sigemptyset(&act.sa_mask);
int ret = sigaction(SIGALRM, &act, 0); // 注册SIGALRM信号
if (-1 == ret) {
printf("sigaction error!\n");
perror("reasion:");
exit(-1);
}
// 把当前进程挂起,直到接收到任何一个信号
pause();
if (workflag) {
printf("父进程结束挂起!\n");
}
}
int status = 0;
wait(&status);
return 0;
}
2). alarm 函数
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
描述:在指定的时间之内给进程本身发送一个SIGALRM信号;
参数:
seconds
秒数;
返回值:
成功:返回上一次alarm执行后剩余的时间(秒),如果没有,则返回0;
失败:返回-1;
注意:
参数单位是秒,如果参数为0,则取消已设置的闹钟;如果执行了alarm,但时间还没有到,再次调用alarm,则闹钟将重新定时,每个进程最多只能使用一个闹钟,也就是只能使用一个alarm;
也可以说alarm是一个闹钟,也可以说是定时器!
例:
进程调用pause()函数挂起,调用alarm(3),3秒后唤醒进程!
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
int workflag = 0;
void wake_handle(int sig) {
workflag = 1;
}
int main(int argc, char **argv) {
int ret = 0;
struct sigaction act;
act.sa_handler = wake_handle;
act.sa_flags = 0;
sigemptyset(&act.sa_mask);
sigaction(SIGALRM, &act, 0); // 注册SIGALRM信号
ret = alarm(3); // 3秒后给自己发送SIGALRM信号
if (-1 == ret) {
printf("alarm error!\n");
exit(-1);
}
printf("进程开始挂起(睡眠)\n");
pause(); // 把当前进程挂起,直到接收到任何一个信号
if (workflag) {
printf("进程被唤醒!\n");
}
return 0;
}
3). raise 函数
#include <signal.h>
int raise(int sig);
描述:给本进程自身发送信号;
参数:
sig
信号;
返回值:
成功:返回0;
失败:返回非0;
例:
使用raise给自身进程发送SIGUSR1信号;
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
void wake_handle(int sig) {
printf("接收到信号:%d\n", sig);
}
int main(int argc, char **argv) {
int ret = 0;
struct sigaction act;
act.sa_handler = wake_handle;
act.sa_flags = 0;
sigemptyset(&act.sa_mask);
sigaction(SIGUSR1, &act, 0); // 注册SIGUSR1信号
// 给自身进程发送SIGUSR1信号
ret = raise(SIGUSR1); // 相当于 kill(getpid(), SIGUSR1);
if (0 != ret) {
printf("raise error!\n");
exit(-1);
}
return 0;
}
5. 发送多个信号的情况
情况一
某进程正在执行某个信号对应的操作函数期间(该信号的安装函数),如果此时,该进程又多次收到同一个信号(同一种信号值的信号),
则:如果该信号是不可靠信号(小于32),则只能再响应一次(即加上正在执行的一次,最多是响应两次,其他都丢弃)。
如果该信号是可靠信号(大于32),则能再响应多次(不会遗漏)。但是,都是都必须等该次响应函数执行完之后,才能响应下一次。
情况二
某进程正在执行某个信号对应的操作函数期间(该信号的安装函数),如果此时,该进程收到另一个信号(不同信号值的信号);
如果该信号被包含在当前信号的signaction的sa_mask(信号屏蔽集)中,则不会立即处理该信号。直到当前的信号处理函数执行完之后,才去执行该信号的处理函数。(如果也是不可靠的信号,那么最多也只会再响应多一次)
否则:则立即中断当前执行过程(如果处于睡眠,比如sleep, 则立即被唤醒)而去执行这个新的信号响应。新的响应执行完之后,再在返回至原来的信号处理函数继续执行。
例:
用以下代码,就可以测试上面两种情况的真实性;
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
void myhandle1(int sig) {
printf("myhandle1收到了信号:%d\n", sig);
int i = 0;
for (; i < 10; i++) {
sleep(1);
}
printf("myhandle1信号处理函数结束:%d\n", sig);
}
void myhandle2(int sig) {
printf("myhandle2 Catch a signal:%d\n", sig);
int i = 0;
for (; i < 10; i++) {
sleep(1);
}
printf("myhandle2 Catch end!:%d\n", sig);
}
int main(int argc, char **argv) {
int ret = 0;
struct sigaction act, act2;
act.sa_handler = myhandle1;
act.sa_flags = 0;
sigemptyset(&act.sa_mask);
sigaddset(&act.sa_mask, SIGUSR2); // 信号集SIGUSR2
sigaction(SIGUSR1, &act, 0); // 注册SIGUSR1信号
act2.sa_handler = myhandle2;
sigemptyset(&act2.sa_mask);
act2.sa_flags = 0;
sigaction(SIGUSR2, &act2, 0); // 注册SIGUSR2信号
while (1) { sleep(1); }
return 0;
}
测试情况一,使用信号SIIGUSR2去测试,连续发送多条SIGUSR2信号,测试到底是不是像上面说的那样!
可以看到,发送了多条SIGUSR2之后,程序也只是做了两次响应,且是排队响应的!测试达到预期!
测试情况二,包含信号屏蔽集;首先发送SIGUSR1信号后,接连发送多条SIGSUR2信号,因为SIGSUR2信号在信号集中,测试是否等待排队响应!
可以看到,发送了多条SIGUSR2之后,程序也只是做了两次响应,且是排队响应的!测试达到预期!
测试情况二,不包含信号屏蔽集;现在将代码中sigaddset(&act.sa_mask, SIGUSR2);这句代码给注释掉,首先发送SIGUSR1信号后,接连发送多条SIGSUR2信号,因为SIGSUR2信号在信号集中,测试是否会中断当前的执行,而去执行新的信号响应;
当发送SIGUSR1信号后,右边程序立即大于“收到了信号:10”,当发送SIGUSR2信号后,立即中断SIGUSR1信号的响应操作,转而执行SIGUSR2信号的响应函数,执行完毕之后,才回来继续执行SIGUSR1信号的响应函数!测试达到预期!
注意:以上三个测试都是使用小于32的信号进行测试,所以即使发送了多个相同的信号,也只是执行了一次而已!
6. 信号集
什么是信号集?
信号集,用sigset_t类型表示,实质是一个无符号长整形;用来表示包含多个信号的集合。
信号集的基本操作
1). sigemptyset
#include <signal.h>
int sigemptyset(sigset_t *set);
描述:把信号集清空;
参数:
set
&act.sa_mask
返回值:
成功:返回0;
失败:返回-1,并设置erron错误标志;
例:
struct sigaction act;
int ret = sigemptyset(&act.sa_mask);
if (-1 == ret) {
printf("sigemptyset error!\n");
perror("reason:"); // # include <errno.h>
}
2). sigfillset
#include <signal.h>
int sigfillset(sigset_t *set);
描述:把所有已经定义的全部信号填充到指定信号集;
参数:
set
&act.sa_mask
返回值:
成功:返回0;
失败:返回-1,并设置erron错误标志;
例:
struct sigaction act;
int ret = sigfillset(&act.sa_mask);
if (-1 == ret) {
printf("sigfillset error!\n");
perror("reason:"); // # include <errno.h>
}
3). sigdelset
#include <signal.h>
int sigdelset(sigset_t *set, int signum);
描述:从指定的信号集中删除指定的信号;
参数:
set
&act.sa_mask
signum
信号;
返回值:
成功:返回0;
失败:返回-1,并设置erron错误标志;
例:
struct sigaction act;
int ret = sigdelset(&act.sa_mask, SIGUSR2);
if (-1 == ret) {
printf("sigdelset error!\n");
perror("reason:"); // # include <errno.h>
}
4). sigaddset
#include <signal.h>
int sigaddset(sigset_t *set, int signum);
描述:从指定的信号集中添加指定的信号;
参数:
set
&act.sa_mask
signum
信号;
返回值:
成功:返回0;
失败:返回-1,并设置erron错误标志;
例:
struct sigaction act;
int ret = sigaddset(&act.sa_mask, SIGUSR2);
if (-1 == ret) {
printf("sigaddset error!\n");
perror("reason:"); // # include <errno.h>
}
5). sigismember
#include <signal.h>
int sigismember(const sigset_t *set, int signum);
描述:判断指定的信号是否在指定的信号集中;
参数:
set
&act.sa_mask
signum
信号;
返回值:
成功:如果是,返回1;如果不是,返回0;
失败:返回-1,并设置erron错误标志;
例:
struct sigaction act;
int ret = sigismember(&act.sa_mask, SIGUSR1);
if (-1 == ret) {
printf("sigismember error!\n");
perror("reason:"); // # include <errno.h>
}
7. 进程的“信号屏蔽字”
进程的“信号屏蔽字”是一个信号集;
向目标进程发送某信号时,如果这个信号在目标进程的信号屏蔽字中,则目标进程将不会捕获到该信号,即不会执行该信号的处理函数。
当该进程的信号屏蔽字不在包含该信号时,则会捕获这个早已收到的信号(执行对应的函数)
1). 修改进程的“信号屏蔽字”
使用 sigprocmask
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
描述:修改和设置信号屏蔽字
参数:
how
SIG_BLOCK 把参数set中的信号添加到信号屏蔽字中;
SIG_UNBLOCK 把参数set中的信号从信号屏蔽字中删除;
SIG_SETMASK 把参数set中的信号设置为信号屏蔽字,之前设置的作废;
set
信号屏蔽字集,&set
oldset
返回之前设置的信号屏蔽字(备份之前的),&oldset
返回值:
成功:返回0;
失败:返回-1,并设置erron错误标志;
例:
SIGINT信号注册后,再定义信号屏蔽字,设置SIGINT信号屏蔽字,休眠5秒,这5秒内,狂按Ctrl+c都是没反应的,因为这个信号被设置了信号屏蔽字;5秒后,删除信号屏蔽字,则会捕获这个早已收到的信号(执行对应的函数),但只会执行一次(猜测是信号小于32的原因)
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
void myhandle(int sig)
{
printf("Catch a signal : %d\n", sig);
printf("Catch end.%d\n", sig);
}
int main(void)
{
struct sigaction act;
act.sa_handler = myhandle;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
sigaction(SIGINT, &act, 0);
sigset_t proc_sig_msk, old_mask;
sigemptyset(&proc_sig_msk);
sigaddset(&proc_sig_msk, SIGINT);
// 设置信号屏蔽字
sigprocmask(SIG_BLOCK, &proc_sig_msk, &old_mask);
sleep(5);
printf("had delete SIGINT from process sig mask\n");
// 删除信号屏蔽字
sigprocmask(SIG_UNBLOCK, &proc_sig_msk, &old_mask);
while (1) {
sleep(1);
}
return 0;
}
可以看到,程序运行后,按ctrl+c已经没有反应了,5秒后,之前积累的信号只响应了一次;再按ctrl+c,就正常响应了;测试符合预期!
2). 获取未处理的信号
当进程的信号屏蔽字中信号发生时,这些信号不会被该进程响应,可通过sigpending函数获取这些已经发生了但是没有被处理的信号;
#include <signal.h>
int sigpending(sigset_t *set);
描述:获取被屏蔽未处理的信号
参数:
set
获取未被处理的信号返回,&set
返回值:
成功:返回0;
失败:返回-1,并设置erron错误标志;
例:
sigset_t proc_sig_msk;
int ret = sigpending(&proc_sig_msk);
if (0 != ret) {
printf("sigpending error!\n");
perror("reason:"); // #include <errno.h>
}
8. 阻塞式等待信号
1). pause
#include <unistd.h>
int pause(void);
描述:阻塞进程,直到发生任一信号为止;
返回值:
当接收到任一信号后,返回-1,且errno被设置为EINTR;
例:
pause();
2). sigsuspend
#include <signal.h>
int sigsuspend(const sigset_t *mask);
描述:用指定的参数设置信号屏蔽字,然后阻塞时等待信号的发生。即,只等待信号屏蔽字之外的信号;
参数:
mask
信号屏蔽字集,&mask
返回值:
总是返回-1;
例:
设置信号屏蔽字,屏蔽SIGINT信号,然后注册SIGUSR1信号,调用sigsuspend函数阻塞,最后在另一个终端kill -SIGUSR1
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
void myhandle(int sig)
{
printf("Catch a signal : %d\n", sig);
printf("Catch end.%d\n", sig);
}
void myhandle2(int sig)
{
printf("信号 : %d\n", sig);
}
int main(void)
{
struct sigaction act, act2;
act.sa_handler = myhandle;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
sigaction(SIGINT, &act, 0);
// 防止触发SIGUSR1时程序中断结束,而不是自然结束
act2.sa_handler = myhandle2;
sigemptyset(&act2.sa_mask);
act2.sa_flags = 0;
sigaction(SIGUSR1, &act2, 0);
sigset_t proc_sig_msk, old_mask;
sigemptyset(&proc_sig_msk);
sigaddset(&proc_sig_msk, SIGINT);
// 设置信号屏蔽字
sigprocmask(SIG_BLOCK, &proc_sig_msk, &old_mask);
// 阻塞,等待信号屏蔽集以外的信号触发,才能唤醒
sigsuspend(&proc_sig_msk);
return 0;
}
当接收到SIGINT以外的信号SIGUSR1时,唤醒进程,然后响应信号操作函数,最后结束进程;测试符合预期效果!
二、管道
1. 管道简介
管道,就是进程与进程互相发送和接收消息的媒介!
管道是“半双工”的,即是单向的。
管道是FIFO(先进先出)的。
单进程中的管道:
int fd[2]
使用文件描述符fd[1], 向管道写数据
使用文件描述符fd[0], 从管道读数据
注:单进程中的管道无实际用处;管道用于多进程间通信。
2. 管道的创建
1). pipe
#include <unistd.h>
int pipe(int pipefd[2]);
描述:pipe()创建一个管道,这是一个单向数据通道,可用于进程间通信。数组pipefd用于返回两个文件指管道两端的描述符。pipefd[0]指的是管道的读端;pipefd[1]指的是管道的写端。
参数:
pipefd
int型数组,文件描述符,用于操作读写,即数据发送和接收;0数据接收,1数据发送;
返回值:
成功:返回0;
失败:返回-1,并设置erron错误标志;
注意:获取两个“文件描述符”;分别对应管道的读端和写端。
fd[0]: 是管道的读端
fd[1]: 是管道的写端
如果对fd[0]进行写操作,对fd[1]进行读操作,可能导致不可预期的错误。
例:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int main(int argc, char **argv) {
int fd[2]; // 定义管道fd数组
int ret = 0;
char buff1[1024];
char buff2[1024];
ret = pipe(fd); // 创建一个管道,一个单向数据通道,可用于进程间通信
if (0 != ret) {
printf("create pipe failed!\n");
exit(1);
}
strcpy(buff1, "Hello World!");
write(fd[1], buff1, strlen(buff1)); // 发送信息
printf("send information:%s\n", buff1);
bzero(buff2, sizeof(buff2)); // 清零
// 如果没有消息,会阻塞
read(fd[0], buff2, sizeof(buff2)); // 读取消息
printf("recived information:%s\n", buff2);
return 0;
}
2). 例一 :多进程使用管道通信
创建子进程,父进程给子进程发送消息,子进程接收后,给父进程发送消息,父进程接收消息!
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
int main(int argc, char **argv) {
int fd1[2]; // 父进程:fd1[1] 子进程:fd1[0]
int fd2[2]; // 父进程:fd2[0] 子进程:fd2[1]
int ret = 0;
char buff1[1024];
char buff2[1024];
pid_t pd; // 进程id
ret = pipe(fd1); // 创建一个管道,一个单向数据通道,可用于进程间通信
if (0 != ret) {
printf("create pipe1 failed!\n");
exit(1);
}
ret = pipe(fd2);
if (0 != ret) {
printf("create pipe2 failed!\n");
exit(2);
}
pd = fork();
if (-1 == pd) {
printf("fork error!\n");
exit(3);
} else if (0 == pd) {
/* 收消息 */
bzero(buff2, sizeof(buff2)); // 清零
read(fd1[0], buff2, sizeof(buff2)); // 读取消息
printf("%d[Child] 进程收到消息:%s\n", getpid(), buff2);
/* 发消息 */
strcpy(buff1, "Hello parent!");
write(fd2[1], buff1, sizeof(buff1)); // 发送消息
printf("%d[Child] 进程发送消息:%s\n", getpid(), buff1);
} else {
/* 发消息 */
strcpy(buff1, "Hello child!");
write(fd1[1], buff1, sizeof(buff1));
printf("%d[Parent] 进程发送消息:%s\n", getpid(), buff1);
/* 收消息 */
bzero(buff2, sizeof(buff2));
read(fd2[0], buff2, sizeof(buff2));
printf("%d[Parent] 进程收到消息:%s\n", getpid(), buff2);
}
wait();
return 0;
}
3). 例二:关闭管道的读端/写端
管道关闭后的读操作:
问题:
对管道进行read时,如果管道中已经没有数据了,此时读操作将被“阻塞”。
如果此时管道的写端已经被close了,则读操作将可能被一直阻塞!
而此时的阻塞已经没有任何意义了。(因为管道的写端已经被关闭,即不会再写入数据了)
解决方案:
如果不准备再向管道写入数据,则把该管道的所有写端都关闭,
则,此时再对该管道read时,就会返回0,而不再阻塞该读操作。(管道的特性)
注意,这是管道的特性。
如果有多个写端口,而只关闭了一个写端,那么无数据时读操作仍将被阻塞。
实际实现方式:
父子进程各有一个管道的读端和写端;
把父进程的读端(或写端)关闭;
把子进程的写端(或读端)关闭;
使这个“4端口”管道变成单向的“2端口”管道,如图:
例:
子进程没有发送消息的需求,关闭发送端口;然后开始接收消息,收到消息后休眠一秒后再进行一次接收消息;
父进程没有接收消息的希求,关闭接收端口;然后给子进程发送一条消息后休眠三秒,然后关闭发送端口;
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
int main(int argc, char **argv) {
int fd[2];
int ret = 0;
char buff1[1024];
char buff2[1024];
pid_t pd; // 进程id
ret = pipe(fd); // 创建一个管道,一个单向数据通道,可用于进程间通信
if (0 != ret) {
printf("create pipe1 failed!\n");
exit(1);
}
pd = fork();
if (-1 == pd) {
printf("fork error!\n");
exit(3);
} else if (0 == pd) {
close(fd[1]); // 如果子进程没有发送信息需求,关闭子进程的发送消息管道
/* 收消息 */
bzero(buff2, sizeof(buff2)); // 清零
read(fd[0], buff2, sizeof(buff2)); // 读取消息
printf("%d 进程收到消息:%s\n", getpid(), buff2);
sleep(1);
/* 收消息 */
bzero(buff2, sizeof(buff2)); // 清零
ret = read(fd[0], buff2, sizeof(buff2)); // 此时这里会阻塞,直到收到消息,或者发送端被关闭
if (ret > 0) {
printf("%d[Child] 进程收到消息:%s\n", getpid(), buff2);
} else if (0 == ret) {
printf("[%d] 发送消息端口已经被关闭!\n", ret);
}
close(fd[0]);
} else {
close(fd[0]); // 父进程不需要收消息,关闭收消息管道
/* 发消息 */
strcpy(buff1, "Hello child!");
write(fd[1], buff1, sizeof(buff1));
printf("%d[Parent] 进程发送消息:%s\n", getpid(), buff1);
sleep(3);
close(fd[1]); // 关闭父进程的发消息管道
}
wait(NULL);
return 0;
}
4). 例三:父进程循环给子进程发送消息
创建一个子进程,父进程通过管道向子进程发送数据(字符串),该字符串由用户输入。
当用户输入”exit”时, 就不再向子进程发送数据,并关闭该端的管道。
子进程从管道读取数据,并输出。
直到父进程关闭了管道的写端。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
int main(int argc, char **argv) {
int fd[2];
int ret = 0;
char buff1[1024];
char buff2[1024];
pid_t pd; // 进程id
ret = pipe(fd); // 创建一个管道,一个单向数据通道,可用于进程间通信
if (0 != ret) {
printf("create pipe1 failed!\n");
exit(1);
}
pd = fork();
if (-1 == pd) {
printf("fork error!\n");
exit(3);
} else if (0 == pd) {
close(fd[1]); // 如果子进程没有发送信息需求,关闭子进程的发送消息管道
while (1) {
/* 收消息 */
bzero(buff2, sizeof(buff2)); // 清零
ret = read(fd[0], buff2, sizeof(buff2)); // 此时这里会阻塞,直到收到消息,或者发送端被关闭
if (ret > 0) {
printf("%d[Child] 进程收到消息:%s\n", getpid(), buff2);
} else if (0 == ret) {
printf("[%d] 发送消息端口已经被关闭!\n", ret);
break;
}
}
} else {
while (1) {
// 父进程不需要收消息,关闭收消息管道
close(fd[0]);
bzero(buff1, sizeof(buff1)); // 清零
scanf("%s", buff1);
if (0 == strcmp(buff1, "exit")) {
close(fd[1]); // 关闭父进程的发消息管道
break;
} else {
/* 发消息 */
write(fd[1], buff1, sizeof(buff1));
printf("%d[Parent] 进程发送消息:%s\n", getpid(), buff1);
}
}
}
wait(NULL);
return 0;
}
3. popen / pclose
popen的作用:
用来在两个程序之间传递数据:
在程序A中使用popen调用程序B时,有两种用法:
- 程序A读取程序B的输出(使用fread读取)
- 程序A发送数据给程序B,以作为程序B的标准输入。(使用fwrite写入)
1). popen
#include <stdio.h>
FILE *popen(const char *command, const char *type);
描述:创建一个管道流;
参数:
command
可以是shell命令或者程序的名字;
type
可以是r表示读,w表示写;
返回值:
如果内存分配失败,popen()函数不会设置errno。如果底层进程或管道失败,errno被适当设置。如果类型参数无效,当检测到此条件时,errno被设置为EINVAL;其他失败返回空;成功返回文件指针!
2). pclose
#include <stdio.h>
int pclose(FILE *stream);
描述:关闭管道流;
参数:
stream
管道(文件)流;
返回值:
成功:返回0;
失败:返回-1,并设置erron错误标志;
3). 例一:读取命令返回的数据
#include <stdio.h>
#include <stdlib.h>
#define BUFF_SIZE 1024
int main(void) {
FILE *file;
char buff[BUFF_SIZE + 1] = { '\0' };
int cnt;
// 管道方式以读的方式打开这条命令
file = popen("ls -l", "r");
if (!file) {
printf("popen failed!\n");
exit(1);
}
// 读取"ls -l"显示的所有数据
cnt = fread(buff, sizeof(char), BUFF_SIZE, file);
if (cnt > 0) {
buff[cnt] = '\0';
printf("%s", buff);
}
// 关闭
int ret = pclose(file);
printf("ret = %d\n", ret);
return 0;
}
4). 例二:把输出写到外部程序
pipe_6.cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define BUFF_SIZE 1024
int main(void) {
FILE *file;
char buff[BUFF_SIZE + 1] = { '\0' };
int cnt;
// 管道方式以写的方式运行p2程序
file = popen("./p2", "w");
if (!file) {
printf("popen failed!\n");
exit(1);
}
strcpy(buff, "hello world!");
cnt = fwrite(buff, sizeof(char), strlen(buff), file);
if (cnt > 0) {
printf("send \"%s\" successed!\n", buff);
}
// 关闭
pclose(file);
return 0;
}
p2.cpp
#include <stdio.h>
#include <unistd.h>
#define BUFF_SIZE 1024
int main(void) {
char buff[BUFF_SIZE] = { '\0' };
int ret = 0;
ret = read(0, buff, sizeof(buff));
if (ret > 0) {
buff[ret] = '\0';
printf("buff = %s\n", buff);
}
return 0;
}
编译命令:
gcc pipe_6.cpp
gcc p2.cpp -o p2
popen的原理
先使用fork创建一个子进程,
然后在子进程中使用exec执行指定外部程序,并返回一个文件指针FILE*给父进程。
当使用”r”时,该FILE指向外部程序的标准输出
当使用”w”时,该FILE指向外部程序的标准输入。
5). popen的优缺点
优点:可以使用shell扩展(比如命令中可以使用通配符),使用方便。
缺点:每调用一次popen, 将要启动两个进程(shell和被指定的程序), 资源消耗大。
如果所有管道写端对应的文件描述符被关闭,则read返回0
如果所有管道读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE
三、消息队列
1. 什么是消息队列?
消息队列,用于从一个进程向另一个进程发送数据。
但仅把数据发送到一个“队列”中,而不指定由哪个进程来接受。
消息队列,独立于发送消息的进程和接收消息的进程。
(信号、管道、命名管道都不独立于发送和接收进程)
消息队列,有最大长度限制:MSGMNB
消息队列中的单条消息,也有最大长度限制:MSGMAX
简单来讲,就是有一条队列,进程A把消息发送到队列中,进程B就可以在这个队列中去获取接收这条消息!
2. msgget 消息队列的获取
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgget(key_t key, int msgflg);
描述:通过系统调用 创建 | 获取 消息队列;
参数:
key
指定创建或获取消息队列的key,唯一;
msgflag
IPC_CREAT 或者 IPC_CREAT | IPC_EXCL
IPC_CREAT 消息队列不存在则创建,存在则返回;
IPC_CREAT | IPC_EXCL 消息队列不存在则创建,存在则报错返回;
返回值:
成功:返回0;
失败:返回-1,并设置erron错误标志;
具体错误原因可以使用命令:man 2 msgget 去查看.
3. msgsnd 消息的发送
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
描述:发送一个消息,即把消息添加到消息队列中;
参数:
msqid
消息队列标识符;
msgp
消息指针,自己定义的结构体;
注:消息的类型需要自己定义。但要求其第一个结构成员为 long int , 例:
struct msgbuf {
long mtype; /* 消息的类型,取值大于0, 接收消息时可使用该值 */
/* 其他数据变量 */
char mtext[1];
}
msgsz
消息的长度(不包含第一个成员msg_type);
msgflg
如果包含: IPC_NOWAIT,则消息队列满时,不发送该消息,而立即返回-1;
如果不包含:IPC_NOWAIT,则消息队列满时,挂起本进程,直到消息队列有空间可用;
返回值:
成功:返回0;
失败:返回-1,并设置erron错误标志;
具体错误原因可以使用命令:man 2 msgsnd 去查看.
4. msgrcv 消息的接收
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
描述:从消息队列中接收一条消息;
参数:
msqid
消息队列标识符;
msgp
消息指针,自己定义的结构体,用于接收消息的缓存;
msgsz
消息的长度(不包含第一个成员msg_type);
msgtyp
指定接收消息的类型:
0:从消息队列中获取第一个消息,以实现顺序接受(先发先收);
>0:从消队列中获取相同类型的第一个消息;
<0:从消息队列中获取消息类型<=(msgtyep的绝对值)的第一个消息;
msgflg
如果包含:IPC_NOWAIT,则当消息队列中没有指定类型的消息时,立即返回-1;
如果不包含:IPC_NOWAIT,则当消息队列中没有指定类型的消息时,挂起本进程,直到收到指定类型的消息;
返回值:
成功:返回接收到的消息的长度(不包含第一个成员msg_type)
失败:返回-1,并设置erron错误标志;
具体错误原因可以使用命令:man 2 msgrcv 去查看.
5. msgctl 消息的控制
一般用来关闭消息队列!
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
描述:控制消息队列,例如关闭消息队列;
参数:
msqid
消息队列标识符;
cmd
IPC_RMID 删除消息队列;具体其他的可以使用命令:man 2 msgctl 去查看;
buf
结构体,存储消息队列的一些信息,使用IPC_RMID传0即可;
返回值:
成功:返回0;
失败:返回-1,并设置erron错误标志;
具体错误原因可以使用命令:man 2 msgctl 去查看.
6. 示例一:两程序间通过消息队列去通信
msg1.cpp
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MSG_SIZE 80
#define MSG_TYPE long
typedef struct MY_MSG_ST {
MSG_TYPE msg_type; // 必须定义的一个变量,用于辨认类型
char msg[MSG_SIZE]; // 数据
}my_msg_st;
int main(int argc, char **argv) {
int msgId; // 消息队列id
int ret;
my_msg_st msg;
// IPC_CREAT 消息队列不存在则创建,存在则返回;
// IPC_CREAT | IPC_EXCL 消息队列不存在则创建,存在则报错返回;
msgId = msgget((key_t)12356, 0666 | IPC_CREAT);
if (-1 == msgId) {
printf("msgget failed!\n");
exit(1);
}
msg.msg_type = 1; // 设置要发送的消息的类型
strcpy(msg.msg, "Hello World!");
// 发送一条消息
ret = msgsnd(msgId, &msg, sizeof(my_msg_st) - sizeof(MSG_TYPE), 0);
if (-1 == ret) {
printf("msgsnd failed!\n");
exit(2);
}
return 0;
}
msg2.cpp
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MSG_SIZE 80
#define MSG_TYPE long
typedef struct MY_MSG_ST {
MSG_TYPE msg_type; // 必须定义的一个变量,用于辨认类型
char msg[MSG_SIZE]; // 数据
}my_msg_st;
int main(int argc, char **argv) {
int msgId; // 消息队列id
int ret;
my_msg_st msg;
// IPC_CREAT 消息队列不存在则创建,存在则返回;
// IPC_CREAT | IPC_EXCL 消息队列不存在则创建,存在则报错返回;
msgId = msgget((key_t)12356, 0666 | IPC_CREAT);
if (-1 == msgId) {
printf("msgget failed!\n");
exit(1);
}
msg.msg_type = 0; // 设置要接收的消息的类型
// 接收一条消息
ret = msgrcv(msgId, &msg, sizeof(my_msg_st) - sizeof(MSG_TYPE), 0, 0);
if (-1 == ret) {
printf("msgrcv failed!\n");
exit(2);
}
printf("recived:%s\n", msg.msg);
// 删除消息队列
ret = msgctl(msgId, IPC_RMID, 0);
if (-1 == ret) {
printf("msgctl(IPC_RMID) failed!\n");
exit(3);
}
return 0;
}
7. 多进程间消息队列通信
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <unistd.h>
#include <wait.h>
#include <errno.h>
#define MSG_SIZE 80
#define MSG_TYPE long
typedef struct MY_MSG_ST {
MSG_TYPE msg_type; // 必须定义的一个变量,用于辨认类型
char msg[MSG_SIZE]; // 数据
}my_msg_st;
int main(int argc, char **argv) {
int msgId; // 消息队列id
int ret;
my_msg_st snd_msg; // 发送数据
my_msg_st rcv_msg; // 接收数据
pid_t pd;
// IPC_CREAT 消息队列不存在则创建,存在则返回;
// IPC_CREAT | IPC_EXCL 消息队列不存在则创建,存在则报错返回;
msgId = msgget((key_t)987, 0666 | IPC_CREAT | IPC_EXCL);
if (-1 == msgId) {
printf("msgget failed!\n");
exit(1);
}
// 创建子进程
pd = fork();
if (-1 == pd) {
printf("fork error!\n");
exit(2);
} else if (0 == pd) {
rcv_msg.msg_type = 0; // 设置要接收的消息的类型
// 接收一条消息
ret = msgrcv(msgId, &rcv_msg, sizeof(my_msg_st) - sizeof(MSG_TYPE), 0, 0);
if (-1 == ret) {
printf("msgrcv failed!\n");
exit(2);
}
printf("recived:%s\n", rcv_msg.msg);
} else {
snd_msg.msg_type = 999; // 设置要发送的消息的类型
strcpy(snd_msg.msg, "This is Message!");
// 发送一条消息
ret = msgsnd(msgId, &snd_msg, sizeof(my_msg_st) - sizeof(MSG_TYPE), 0);
if (-1 == ret) {
printf("msgsnd failed!\n");
exit(3);
}
if (0 < pd) wait(NULL);
// 删除消息队列
ret = msgctl(msgId, IPC_RMID, NULL);
if (-1 == ret) {
printf("msgctl(IPC_RMID) failed!\n");
exit(4);
}
}
return 0;
}
8. 练习
程序1, 循环等待用户输入字符串,每收到一个字符串,就把它发送给进程2,直到用户输入exit;
程序2, 接受进程1发过来的信息,并打印输出;直到接受到exit。
程序一
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MSG_SIZE 80
#define MSG_TYPE long
typedef struct MY_MSG_ST {
MSG_TYPE msg_type; // 必须定义的一个变量,用于辨认类型
char msg[MSG_SIZE]; // 数据
}my_msg_st;
int main(int argc, char **argv) {
int msgId; // 消息队列id
int ret;
my_msg_st msg;
// IPC_CREAT 消息队列不存在则创建,存在则返回;
// IPC_CREAT | IPC_EXCL 消息队列不存在则创建,存在则报错返回;
msgId = msgget((key_t)258, 0666 | IPC_CREAT);
if (-1 == msgId) {
printf("msgget failed!\n");
exit(1);
}
msg.msg_type = 66; // 设置要发送的消息的类型
while (1) {
fgets(msg.msg, sizeof(msg.msg), stdin);
// 发送一条消息
ret = msgsnd(msgId, &msg, sizeof(my_msg_st) - sizeof(MSG_TYPE), 0);
if (-1 == ret) {
printf("msgsnd failed!\n");
exit(2);
}
if (0 == strncmp(msg.msg, "exit", 4)) break;
}
// 删除消息队列
ret = msgctl(msgId, IPC_RMID, NULL);
if (-1 == ret) {
printf("msgctl(IPC_RMID) failed!\n");
exit(3);
}
return 0;
}
程序二
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MSG_SIZE 80
#define MSG_TYPE long
typedef struct MY_MSG_ST {
MSG_TYPE msg_type; // 必须定义的一个变量,用于辨认类型
char msg[MSG_SIZE]; // 数据
}my_msg_st;
int main(int argc, char **argv) {
int msgId; // 消息队列id
int ret;
my_msg_st msg;
// IPC_CREAT 消息队列不存在则创建,存在则返回;
// IPC_CREAT | IPC_EXCL 消息队列不存在则创建,存在则报错返回;
msgId = msgget((key_t)258, 0666 | IPC_CREAT);
if (-1 == msgId) {
printf("msgget failed!\n");
exit(1);
}
msg.msg_type = 66; // 指定接收的数据类型是66的
while (1) {
// 接收一条消息
ret = msgrcv(msgId, &msg, sizeof(my_msg_st) - sizeof(MSG_TYPE), 0, 0);
if (-1 == ret) {
printf("msgrcv failed!\n");
exit(2);
}
if (0 == strncmp(msg.msg, "exit", 4)) break;
printf("recived:%s", msg.msg);
}
return 0;
}
四、信号量
问题:
程序中,有时存在一种特殊代码,最多只允许一个进程执行该部分代码。
这部分区域,称为“临界区”;
然而在多进程并发执行时,当一个进程进入临界区,因某种原因被挂起时,其他进程就有可能也进入该区域。
解决办法:使用信号量。
1. 什么是信号量
信号量,是一种特殊的变量。
只能对信号量执行P操作和V操作;
P操作, 如果信号量的值 > 0, 则把该信号量减1;
如果信号量的值 == 0, 则挂起该进程。
V操作: 如果有进程因该信号量而被挂起,则恢复该进程运行;
如果没有进程因该信号量而挂起,则把该信号量加1。
注意:P操作、V操作都是原子操作,即其在执行时,不会被中断。
注意:此指的“信号量”是指System V IPC的信号量,与线程所使用的信号量不同。该信号量,用于进程间通信。
2. semget 信号量的获取
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semget(key_t key, int nsems, int semflg);
描述:获取System V信号量集标识符;
参数:
key
键值,该键值对应一个唯一的信号量。类似于共享内存的键值;
不同的进程可通过该键值和semget获取唯一的信号量;
特殊键值:IPC_PRIVAT该信号量只允许创建者本身, 可用于父子进程间通信;
nsems
需要的信号量数目,一般取1;
semflg
IPC_CREAT 或者 IPC_CREAT | IPC_EXCL
IPC_CREAT 消息队列不存在则创建,存在则返回;
IPC_CREAT | IPC_EXCL 消息队列不存在则创建,存在则报错返回;
返回值:
成功,返回也给非负整数;
失败,返回-1,并设置错误标志errno;
3. semop 信号量的操作
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semop(int semid, struct sembuf *sops, unsigned nsops);
描述:改变信号量的值,即对信号量执行 P操作 或 V操作;
参数:
semid
信号量标识符,即semget的返回值;
sops
是一个数组,元素类型为struct sembuf;
struct sembuf {
short sem_num; // 信号量组中的编号(即指定对哪个信号量操作)
// semget实际是获取一组信号量
// 如果只获取了一个信号量,则该成员取0
short sem_op; // -1, 表示P操作
// 1, 表示V操作
short sem_flg; // SEM_UNDO : 如果进程在终止时,没有释放信号量;
// 如果不设置指定标志,应该设置为0;则,自动释放该信号量
}
nsops
表示第二个参数sops所表示的数组的大小,即表示有几个struct sembuf;
返回值:
成功:返回0;
失败:返回-1,并设置错误标志errno;
4. semctl 信号量的控制
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semctl(int semid, int semnum, int cmd, ...);
描述:对信号量进行控制;
参数:
semid
信号量标识符;
semnum
信号量组中的编号,如果只有一个信号量,则取值0;
cmd
SETVAL,把信号量初始化为指定的值,具体的值由第四个参数确定;
注意:只能对信号量初始化一次,如果在个进程中,分别对该信号量进行初始化,则可能会导致错误!
IPC_RMID,删除信号量;
参数四类型为:union semun {
int val; // SETVAL 命令要设置的值
struct semid_ds *buf;
unsigned short *array;
}
注意:union semun 类型要求自己定义有些Linux发行版在sys/sem.h中定义,有些发行版则没有定义。
可自己定义如下:
#if defined(__GNU_LIBRARY__) && !defined(_SEM_SEMUN_UNDEFINED)
#else
union semun {
int val;
struct semid_ds *buf;
unsigned short int *array;
struct seminfo *__buf;
};
#endif
返回值:
成功:大部分返回0;
失败:返回-1,并设置错误标志;
5. 例
首先看下面的代码,父进程创建一个子进程后,一起运行for循环...
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
int main(void) {
int i = 0;
pid_t pd = fork();
for (i = 0; i < 5; i++) {
// 模拟临界区 - begin
printf("Process(%d) In\n", getpid());
sleep(1);
printf("Process(%d) Out\n", getpid());
// 模拟临界区 - end
sleep(1);
}
return 0;
}
假设把for循环中sleep(1)比作是厕所的话,父进程进去了,肯定不希望子进程也进去,要等父进程出来,子进程才能进去才对;但是上面的程序是都挤进去了,所以下面使用信号量改良一下!
#include <sys/types.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#if defined(__GNU_LIBRARY__) && !defined(_SEM_SEMUN_UNDEFINED)
#else
union semun {
int val;
struct semid_ds *buf;
unsigned short int *array;
struct seminfo *__buf;
};
#endif
static int sem_initial(int semid) {
int ret;
union semun semun;
semun.val = 1; // 信号量置为1
ret = semctl(semid, 0, SETVAL, semun);
if (-1 == ret) {
fprintf(stderr, "sectl failed!\n");
}
return ret;
}
static int sem_p(int semid) {
int ret;
struct sembuf sembuf;
sembuf.sem_op = -1; // 信号量减一
sembuf.sem_num = 0;
sembuf.sem_flg = SEM_UNDO;
ret = semop(semid, &sembuf, 1);
if (-1 == ret) {
fprintf(stderr, "sem_p failed!\n");
}
return ret;
}
static int sem_v(int semid) {
int ret;
struct sembuf sembuf;
sembuf.sem_op = 1; // 信号量加一
sembuf.sem_num = 0;
sembuf.sem_flg = SEM_UNDO;
ret = semop(semid, &sembuf, 1);
if (-1 == ret) {
fprintf(stderr, "sem_v failed!\n");
}
return ret;
}
int main(int argc, char **argv) {
int i;
int ret;
int semid;
/* 获取信号量 */
semid = semget((key_t)1234, 1, 0666|IPC_CREAT);
if (-1 == ret) {
fprintf(stderr, "semget failed!\n");
exit(1);
}
/* 初始化信号量 */
if (argc > 1) {
ret = sem_initial(semid);
if (-1 == ret) {
exit(2);
}
}
for (i = 0; i < 5; i++) {
if (-1 == sem_p(semid)) {
exit(3);
}
// 进入临界区
printf("Process(%d) In\n", getpid());
sleep(3);
printf("Process(%d) Out\n", getpid());
// 退出临界区
if (-1 == sem_v(semid)) {
exit(4);
}
//sleep(1);
}
sleep(4);
if (argc > 1) {
/* 删除信号量 */
union semun semun;
ret = semctl(semid, 0, IPC_RMID, 0);
if (-1 == ret) {
fprintf(stderr, "semctl failed! reason: %s\n", strerror(errno));
exit(5);
}
}
return 0;
}
父进程In后,子进程只能等待,等到父进程Out之后,子进程才能In;子进程In后,父进程也只能等待,等到子进程Out之后,父进程才能In;
这就是信号量设置的临界区的效果,阻止两个进程同时访问同一段代码!
有点像线程加锁和解锁!
五、共享内存机制
由于篇幅太大了,这部分留到下一篇博客中介绍;共享内存这效率挺高的,项目中也常用!
六、总结
信号 与 管道 与 消息队列 与信号量 的基本用法已经整理完毕,实际用法也在代码中表现出来了!
这里只是将一些入门用法写下来,具体还得自己取深入研究!