什么是进程间通信,就是进程与进程之间进行通信,互相发送消息;可以通过 信号 或者 管道 或者 消息队列 或者 信号量 去通信!
目录
一、信号
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;
这就是信号量设置的临界区的效果,阻止两个进程同时访问同一段代码!
有点像线程加锁和解锁!
五、共享内存机制
由于篇幅太大了,这部分留到下一篇博客中介绍;共享内存这效率挺高的,项目中也常用!
六、总结
信号 与 管道 与 消息队列 与信号量 的基本用法已经整理完毕,实际用法也在代码中表现出来了!
这里只是将一些入门用法写下来,具体还得自己取深入研究!