Linux信号的使用和进程间通信
一、信号的使用
1. 信号的基本概念
信号是系统响应某个条件而产生的事件,进程接收到信号会执行相应的操作
2. 修改信号的响应方式—signal()
在键盘上按下 Ctrl+c 时,会给当前终端前台执行的进程发送 SIGINT 信号,用 signal 修 改 SIGINT 信号的响应方式示例代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <assert.h>
#include<signal.h>
//信号处理函数,收到信号的时候要执行的函数
void fun_sig(int sig)
{
printf("sig=%d\n",sig);
signal(sig,SIG_DFL);
}
int main()
{
//和内核之间达成的约定,如果收到信号,就调用fun_sig函数处理,
signal(SIGINT,fun_sig);
//signal(SIGINT,SIG_IGN);
while(1)
{
printf("hello!\n");
sleep(1);
}
}
3. 发送信号—kill()
int kill(pid_t pid, int sig);
pid > 0
指定将信号发送个那个进程
pid==0
信号被发送到和当前进程在同一个进程组的进程
pid == -1
将信号发送给系统上有权限发送的所有的进程
pid < -1
将信号发送给进程组 id 等于 pid 绝对值,并且有权限发送的所有的进程。
sig
指定发送信号的类型。
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<signal.h>
int main(int argc ,char* argv[])
{
if(argc!=3)
{
printf("arg err\n");
exit(0);
}
int pid=0;
int sig=0;
sscanf(argv[1],"%d",&pid);
sscanf(argv[2],"%d",&sig);
if(kill(pid,sig)==-1)
{
perror("kill err");
}
}
子进程结束会给父进程发送信号 SIGCHILD
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<assert.h>
#include<signal.h>
#include<sys/wait.h>
void fun_sig(int sig)
{
printf("sig=%d\n",sig);
int val=0;
wait(&val);//方法一处理僵死进程
}
int main()
{
//signal(SIGCHLD,fun_sig);
signal(SIGCHLD,SIG_IGN);//方法2处理僵死进程
pid_t pid=fork();
assert(pid!=-1);
if(pid==0)
{
sleep(3);
exit(0);
}
for(int i=0;i<5;++i)
{
printf("main tun\n");
sleep(1);
}
}
二、 进程间通信(IPC)
实现mybash
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<assert.h>
#include<signal.h>
#include<sys/wait.h>
#include<pwd.h>
#define MAX_ARG 10
void print_info()
{
char *s="$";
int id=getuid();
//管理员的id为0
if(0==id)
{
s="#";
}
struct passwd* pw=getpwuid(id);
if(NULL==pw)
{
printf("mybash>>$");
fflush(stdout);
return;
}
char hostname[128]={0};
if(gethostname(hostname,128)==-1)
{
printf("mybash>>$");
fflush(stdout);
return;
}
char pwd_str[256]={0};
if(getcwd(pwd_str,256)==NULL)
{
printf("mybash>>$");
fflush(stdout);
return;
}
printf("\033[1;32m%s\033[1;34m@%s%s\033[0m ",pw->pw_name,hostname,pwd_str,s);
fflush(stdout);
}
//解析命令
char *get_cmd(char *buff,char *myargv[])
{
if(buff==NULL||myargv==NULL)
{
return NULL;
}
int i=0;
char* s=strtok(buff," ");//分隔符是空格
while(s!=NULL)
{
myargv[i++]=s;
s=strtok(NULL," ");
}
return myargv[0];
}
void change_dirent(char *dir)
{
if(dir==NULL)
{
return;
}
if(chdir(dir)==-1)
{
perror("cd err");
}
}
int main()
{
while(1)
{
print_info();
char buff[128]={0};
fgets(buff,128,stdin);//"ls\n"
buff[strlen(buff)-1]=0;//"ls"
char *myargv[MAX_ARG]={0};
char *cmd=get_cmd(buff,myargv);
if(cmd==NULL)
{
continue;
}
else if(strcmp(cmd,"cd")==0)
{
change_dirent(myargv[1]);
continue;
}
else if(strcmp(cmd,"exit")==0)
{
break;
}
else
{
//fork+exec
pid_t pid=fork();
if(pid==-1)
{
printf("fork err\n");
continue;
}
if(pid==0)
{
execvp(cmd,myargv);
perror("execvp err");
exit(0);
}
wait(NULL);
}
}
}
1. 管道
管道可以用来在两个进程之间传递数据,如: ps -ef | grep “bash”, 其中‘|’就是管道,其作用就是将 ps 命令的结果写入管道文件,然后 grep 再从管道文件中读出该数据进行过滤。
无名在父子进程间,有名在任意进程间。
无论有名还是无名,写入管道的数据都在内存中。管道是一种半双工通信方式。
- 有名管道
有名管道可以在任意两个进程之间通信,默认大小4096
有名管道的创建:
◼ 命令创建:mkfifo FIFO
◼ 系统调用创建
- 无名管道
无名管道主要应用于父子进程间的通信。
fd[0]:读端
fd[1]:写端
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<assert.h>
#include<fcntl.h>
int main()
{
int fd[2];
assert(pipe(fd)!=-1);
pid_t pid=fork();
assert(pid!=-1);
if(pid==0)
{
close(fd[1]);
char buff[128]={0};
read(fd[0],buff,127);
printf("child read:%s\n",buff);
close(fd[0]);
}
else
{
close(fd[0]);
write(fd[1],"hello",5);
close(fd[1]);
}
}
2. 信号量
信号量是一个特殊的变量,一般取正数值。它的值代表允许访问的资源数目,获取资源时,需要对信号量的值进行原子减一,该操作被称为 P 操作。当信号量值为 0 时,代表没有资源可用,P 操作会阻塞。释放资源时,需要对信号量的值进行原子加一,该操作被称为 V操作。
信号量主要用来同步进程。信号量的值如果只取 0,1,将其称为二值信号量。如果信号量的值大于 1,则称之为计数信号量。
临界资源:同一时刻,只允许被一个进程或线程访问的资源
临界区:访问临界资源的代码段
可以看见A和B存在交替出现,即访问冲突,可以用信号量同步解决
- 信号量的使用
(1)sem.h
#include<sys/sem.h>
#include<unistd.h>
union semun
{
int val;
};
void sem_init();//创建/已存在的信号量
void sem_p();//加一
void sem_v();//减一
void sem_destroy();//销毁
sem.c
#include "sem.h"
static int semid=-1;
void sem_init()
{
semid=semget((key_t)1234,1,IPC_CREAT|IPC_EXCL|0600);//全新创建信号量
if(semid==-1)//全新创建失败,信号量可能已存在
{
semid=semget((key_t)1234,1,0600);
if(semid==-1)
{
perror("semget error");
}
}
else
{
union semun a;
a.val=1;//信号量的初始值
if(semctl(semid,0,SETVAL,a)==-1)//初始化信号量,用SETVAL将联合体中val的值设置到信号量中
{
perror("semctl init error");
}
}
}
void sem_p()
{
struct sembuf buf;
buf.sem_num=0;
buf.sem_op=-1;
buf.sem_flg=SEM_UNDO;
if(semop(semid,&buf,1)==-1)
{
perror("p error");
}
}
void sem_v()
{
struct sembuf buf;
buf.sem_num=0;
buf.sem_op=1;
buf.sem_flg=SEM_UNDO;
if(semop(semid,&buf,1)==-1)
{
perror("v error");
}
}
void sem_destroy()
{
if(semctl(semid,0,IPC_RMID)==-1)
{
perror("destroy sem error");
}
}
a.c和b.c
操作信号量接口介绍:
int semget(key_t key, int nsems, int semflg);
semget()创建或者获取已存在的信号量
semget()成功返回信号量的 ID, 失败返回-1
key:两个进程使用相同的 key 值,就可以使用同一个信号量
nsems:内核维护的是一个信号量集,在新建信号量时,其指定信号量集中信号量的个数
semflg 可选: IPC_CREAT IPC_EXCL
int semop(int semid, struct sembuf *sops, unsigned nsops);
semop()对信号量进行改变,做 P 操作或者 V 操作
semop()成功返回 0,失败返回-1
struct sembuf
{
unsigned short sem_num; //指定信号量集中的信号量下标
short sem_op; //其值为-1,代表 P 操作,其值为 1,代表 V 操作
short sem_flg; //SEM_UNDO
}
int semctl(int semid, int semnum, int cmd, ...);
semctl()控制信号量
semctl()成功返回 0,失败返回-1
semnum:信号量的下标
cmd 选项: SETVAL IPC_RMID
union semun
{
int val;
struct semid_ds *buf;
unsigned short *array;
struct seminfo *_buf;
};
ipcs/ipcrm
ipcs
可以查看消息队列、共享内存、信号量的使用情况,使用ipcrm
可以进行删除操作。
(2)
sem.h
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<sys/sem.h>
#define SEM_MAX 3
#define SEM1 0
#define SEM2 1
#define SEM3 2
union semun
{
int val;
};
void sem_init();
void sem_p(int index);
void sem_v(int index);
void sem_destroy();
sem.c
#include "sem.h"
static int semid=-1;
void sem_init()
{
int arr[SEM_MAX]={1,0,0};//信号量初始值
semid=semget((key_t)1234,SEM_MAX,IPC_CREAT|IPC_EXCL|0600);
if(semid==-1)
{
semid=semget((key_t)1234,SEM_MAX,0600);
if(semid==-1)
{
printf("semget err\n");
return;
}
}
else{
union semun a;
int i=0;
for(;i<SEM_MAX;++i)
{
a.val=arr[i];
if(semctl(semid,i,SETVAL,a)==-1)
{
printf("semctl setval err\n");
}
}
}
}
void sem_p(int index)
{
if(index<0||index>=SEM_MAX)
{
printf("index err\n");
return;
}
struct sembuf buf;
buf.sem_num=index;
buf.sem_op=-1;
buf.sem_flg=SEM_UNDO;
if(semop(semid,&buf,1)==-1)
{
printf("semop p err\n");
}
}
void sem_v(int index)
{
if(index<0||index>=SEM_MAX)
{
printf("index err\n");
return;
}
struct sembuf buf;
buf.sem_num=index;
buf.sem_op=1;
buf.sem_flg=SEM_UNDO;
if(semop(semid,&buf,1)==-1)
{
printf("semop v err\n");
}
}
void sem_destroy()
{
if(semctl(semid,0,IPC_RMID)==-1)
{
printf("sem destroy err\n");
}
}
3. 共享内存
共享内存为多个进程之间共享和传递数据提供了一种有效的方式。共享内存是先在物理内存上申请一块空间,多个进程可以将其映射到自己的虚拟地址空间中。所有进程都可以访问共享内存中的地址,就好像它们是由 malloc 分配的一样。如果某个进程向共享内存写入了数据,所做的改动将立刻被可以访问同一段共享内存的任何其他进程看到。由于它并未提供同步机制,所以我们通常需要用其他的机制来同步对共享内存的访问。
int shmget(key_t key, size_t size, int shmflg);
shmget()用于创建或者获取共享内存
shmget()成功返回共享内存的 ID, 失败返回-1
key: 不同的进程使用相同的 key 值可以获取到同一个共享内存
size: 创建共享内存时,指定要申请的共享内存空间大小
shmflg: IPC_CREAT IPC_EXCL
void* shmat(int shmid, const void *shmaddr, int shmflg);
shmat()将申请的共享内存的物理内存映射到当前进程的虚拟地址空间
shmat()成功返回返回共享内存的首地址,失败返回 NULL
shmaddr:一般给 NULL,由系统自动选择映射的虚拟地址空间
shmflg: 一般给 0, 可以给 SHM_RDONLY 为只读模式,其他的为读写
int shmdt(const void *shmaddr);
shmdt()断开当前进程的 shmaddr 指向的共享内存映射
shmdt()成功返回 0, 失败返回-1
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
shmctl()控制共享内存
shmctl()成功返回 0,失败返回-1
cmd: IPC_RMID
(1)进程a向共享内存中读入数据,进程b从共享内存中读取数据并显示
a.c
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<assert.h>
#include<sys/shm.h>
int main()
{
int shmid=shmget((key_t)1234,256,IPC_CREAT|0600);
assert(shmid!=-1);
char *s=(char*)shmat(shmid,NULL,0);
assert(s!=(char*)-1);
strcpy(s,"hello");
shmdt(s);
}
b.c
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<assert.h>
#include<sys/shm.h>
int main()
{
int shmid=shmget((key_t)1234,256,IPC_CREAT|0600);
assert(shmid!=-1);
char *s=(char*)shmat(shmid,NULL,0);
assert(s!=(char*)-1);
printf("%s",s);
shmdt(s);
}
(2)进程 a 从键盘循环获取数据并拷贝到共享内存中,进程 b 从共享内存中获取并打印数据。要求进程 a 输入一次,进程 b 输出一次,进程 a 不输入,进程 b 也不输出。
sem.h
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<assert.h>
#include<malloc.h>
#include<sys/shm.h>
#include<sys/sem.h>
#define SEM_W 0
#define SEM_R 1
typedef union SemUn
{
int val;
}SemUn;
int SemGet(int key,int semVal[],int nsems);
int SemP(int semid,int index);
int SemV(int semid,int index);
int SemDel(int semid);
sem.c
#include "sem.h"
int SemGet(int key,int semVal[],int nsems)
{
int semid=semget((key_t)key,nsems,0664);
if(semid==-1)
{
semid=semget((key_t)key,nsems,0664|IPC_CREAT);
if(semid==-1)
{
return -1;
}
int i=0;
for(;i<nsems;i++)
{
SemUn un;
un.val=semVal[i];
if(semctl(semid,i,SETVAL,un)==-1)
{
perror("semctl error");
return -1;
}
}
}
return semid;
}
int SemP(int semid,int index)
{
struct sembuf buf;
buf.sem_num=index;
buf.sem_op=-1;
buf.sem_flg=SEM_UNDO;
if(semop(semid,&buf,1)==-1)
{
return -1;
}
return 0;
}
int SemV(int semid,int index)
{
struct sembuf buf;
buf.sem_num=index;
buf.sem_op=1;
buf.sem_flg=SEM_UNDO;
if(semop(semid,&buf,1)==-1)
{
perror("semop error");
return -1;
}
return 0;
}
int SemDel(int semid)
{
if(shmctl(semid,0,IPC_RMID)==-1)
{
perror("semctl del error");
return -1;
}
return 0;
}
a.c
#include "sem.h"
int main()
{
int shmid=shmget((key_t)1234,256,IPC_CREAT|0664);
assert(shmid!=-1);
int semVal[2]={1,0};
int semid=SemGet(1000,semVal,2);
char *s=(char*)shmat(shmid,NULL,0);
assert(s!=(char*)-1);
while(1)
{
char buff[256]={0};
printf("please input:");
fgets(buff,256,stdin);
SemP(semid,SEM_W);
strcpy(s,buff);
SemV(semid,SEM_R);
if(strncmp(s,"end",3)==0)
{
break;
}
}
shmdt(s);
shmctl(shmid,IPC_RMID,NULL);
exit(0);
}
b.c
#include "sem.h"
int main()
{
int shmid=shmget((key_t)1234,256,IPC_CREAT|0664);
assert(shmid!=-1);
int semVal[2]={1,0};
int semid=SemGet(1000,semVal,2);
char *s=(char*)shmat(shmid,NULL,0);
assert(s!=(char*)-1);
while(1)
{
SemP(semid,SEM_R);
if(strncmp(s,"end",3)==0)
{
break;
}
printf("lenth=%d:%s",strlen(s)-1,s);
SemV(semid,SEM_W);
}
shmdt(s);
shmctl(shmid,IPC_RMID,NULL);
SemDel(semid);
exit(0);
}
运行结果
4. 消息队列
使用场景:
- 将消息队列应用在日志处理当中,比如 Kafka的应用,解决大量日志传输的问题。
- 秒杀活动一般会因为流量过大,导致流量暴增,应用挂掉。为解决这个问题,一般需要在应用前端加入消息队列;可以控制活动的人数;可以缓解短时间内高流量压垮应用。
- 异步处理,用户注册后,需要发注册邮件和注册短信。传统的做法有两种,串行和并行方 式。
- 消息通讯是指,消息队列一般都内置了高效的通信机制,因此可以使用在纯粹的消息通信应用中, 比如实现点对点消息交互,或者聊天室等。
int msgget(key_t key, int msqflg);
msgget()创建或者获取一个消息队列
msgget()成功返回消息队列 ID,失败返回-1
msqflg: IPC_CREAT
int msgsnd(int msqid, const void *msqp, size_t msqsz, int msqflg);
msgsnd()发送一条消息,消息结构为:
struct msgbuf
{
long mtype; // 消息类型, 必须大于 0
char mtext[1]; // 消息数据
};
msgsnd()成功返回 0, 失败返回-1
msqsz: 指定 mtext 中有效数据的长度
msqflg:一般设置为 0 可以设置 IPC_NOWAIT
ssize_t msgrcv(int msqid, void *msgp, size_t msqsz, long msqtyp, int msqflg);
msgrcv()接收一条消息
msgrcv()成功返回 mtext 中接收到的数据长度, 失败返回-1
msqtyp: 指定接收的消息类型,类型可以为 0
msqflg: 一般设置为 0 可以设置 IPC_NOWAIT
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
msgctl()控制消息队列
msgctl()成功返回 0,失败返回-1
cmd: IPC_RMID
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<assert.h>
#include<sys/msg.h>
struct mess
{
long type;
char buff[128];
};
int main()
{
int msgid=msgget((key_t)1234,IPC_CREAT|0600);
assert(msgid!=-1);
struct mess dt;
dt.type=1;
strcpy(dt.buff,"hello");
msgsnd(msgid,&dt,128,0);
}
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<assert.h>
#include<sys/msg.h>
struct mess
{
long type;
char buff[128];
};
int main()
{
int msgid=msgget((key_t)1234,IPC_CREAT|0600);
assert(msgid!=-1);
struct mess dt;
msgrcv(msgid,&dt,128,1,0);
printf("read mess:%s\n",dt.buff);
}