目录
发送信号 kill函数 kill函数将信号发送给进程或者进程组。
4.共享内存(share memory)
共享内存(Shared Memory),指两个或多个进程共享一个给定的存储区。
- 使得多个进程可以可以直接读写同一块内存空间,是最快的可用IPC形式。是针对其他通信机制运行效率较低而设计的。
- 为了在多个进程间交换信息,内核专门留出了一块内存区,可以由需要访问的进程将其映射到自己的私有地址空间。进程就可以直接读写这一块内存而不需要进行数据的拷贝,从而大大提高效率。
- 由于多个进程共享一段内存,因此需要依靠某种同步机制(如信号量)来达到进程间的同步及互斥。只有某个进程对内存地址读写完成了,和普通读写一样,strcpy写完马上可以读取,不必登程序解除链接
共享内存原理图
函数:
1 #include <sys/shm.h>
2 // 创建或获取一个共享内存:成功返回共享内存ID,失败返回-1
3 int shmget(key_t key, size_t size, int flag);//size 以兆为单位,最小1024、IPC_CREAT|0666
4 // 连接共享内存到当前进程的地址空间:成功返回指向共享内存的地址,失败返回-1
5 char *shmat(int shm_id, const void *addr, int flag);
//char *add=shmat( id,0/NULL,0) id、映射到的地址、权限
6 // 断开与共享内存的连接:成功返回0,失败返回-1
7 int shmdt(void *addr);
8 // 控制共享内存的相关信息:成功返回0,失败返回-1
9 int shmctl(int shm_id, int cmd, struct shmid_ds *buf);
//对地址操作,写入字符串 shmat返回的地址
strcpy(char *shm,"HELLO! ");
printf("%s\n",char *shm);//shmat返回的地址
当用shmget函数创建一段共享内存时,必须指定其 size;而如果引用一个已存在的共享内存,则将 size 指定为0 。
当一段共享内存被创建以后,它并不能被任何进程访问。必须使用shmat函数连接该共享内存到当前进程的地址空间,连接成功后把共享内存区对象映射到调用进程的地址空间,随后可像本地空间一样访问。shmat返回的是共享内存第一个可用地址,strcpy对内存地址存入字符串,printf打印该地址字符串。 链接完成后,相当于把2个程序放在用一个程序,对地址读写。
shmdt函数是用来断开shmat建立的连接的。注意,这并不是从系统中删除该共享内存,只是当前进程不能再访问该共享内存而已。
shmctl函数可以对共享内存执行多种操作,根据参数 cmd 执行相应的操作。常用的是IPC_RMID(从系统中删除该共享内存)。
相关函数:
1.ftok函数生成键值
每一个共享存储段都有一个对应的键值(key)相关联(消息队列、信号量也同样需要)。
所需头文件:#include<sys/ipc.h>
函数原型 :key_t ftok(const char *path ,int id);
path为一个已存在的路径名
id为0~255之间的一个数值,代表项目ID,自己取
返回值:成功返回键值(相当于32位的int)。出错返回-1
例如:key_t key = ftok( “/tmp”, 66);
2.shmget创建或获取一个共享内存
函数创建共享存储空间并返回一个共享存储标识符
所需头文件:#include<sys/shm.h>
函数原型: int shmget(key_t key, size_t size,int flag);
key:为ftok生成的键值
size:为共享内存的长度,以字节为单位
flag:为所需要的操作和权限,可以用来创建一个共享存储空间并返回一个标识符或者获得一个共享标识符。只有接受的话,可写0
flag的值
IPC_CREAT:如果不存在key值的共享存储空间,且权限不为0,则创建共享存储空间,并返回一个共享存储标识符。如果存在,则直接返回共享存储标识符。
IPC_CREAT | IPC_EXCL:如果不存在key值的共享存储空间,且权限不为0,则创建共享存储空间,并返回一个共享存储标识符。如果存在,则产生错误。
返回值:成功返回共享存储ID;出错返回-1
int id = shmget(key,4096,IPC_CREAT|IPC_EXCL|0666);
int id = shmget(key,4096,IPC_CREAT|0666);
例如:int id = shmget(key,4096,IPC_CREAT|IPC_EXCL|0666);创建一个大小为4096个字节的权限为0666(所有用户可读可写,具体查询linux权限相关内容)的共享存储空间,并返回一个整形共享存储标识符,如果key值已经存在有共享存储空间了,则出错返回-1。
int id = shmget(key,4096,IPC_CREAT|0666);创建一个大小为4096个字节的权限为0666(所有用户可读可写,具体查询linux权限相关内容)的共享存储空间,并返回一个共享存储标识符,如果key值已经存在有共享存储空间了,则直接返回一个共享存储标识符。
3.shmat连接共享内存到当前进程的地址空间,
获取第一个可用共享内存空间的地址,映射到 char *add,就有了操作权限,只不过共享内存不输于进程管,不可以直接 add="fnsajf";
所需头文件:#include<sys/shm.h>
函数原型: char *shmat(int shmid, const void *addr, int flag);
shmid为shmget生成的共享存储标识符
addr:指定要返回的共享内存地址,直接指定为NULL让内核自己决定一个合适的地址位置。一般为0或者NULL。
flag:为对数据的操作,如果指定为SHM_RDONLY则以只读方式连接此段,其他值为读写方式连接此段。一般为0。
返回值:成功返回指向共享存储段的指针;错误返回-1(打印出指针的值为全F)
例如:char *addr = shmat(id, 0, 0);就会返回第一个可用的共享内存地址的指针的值给addr
4.shmdt函数进行分离
当不需要对此共享内存进行操作时候,调用shmdt函数进行分离,不是删除此共享存储空间哟。
所需头文件:#include<sys/shm.h>
函数原型: int shmdt(const void *addr);
addr:为shmat函数返回的地址指针
返回值:成功返回0;错误返回-1
例如:int ret = shmdt(addr);
5.shmctl函数对共享内存进行控制
简单的操作就是删除共享存储空间了,也可以获取和改变共享内存的状态
所需头文件:#include<sys/shm.h>
函数原型:int shmctl(int shmid, int cmd, struct shmid_ds *buf);
shmid:就是shmget函数返回的共享存储标识符
buf: 就是结构体shmid_ds,不关心可以写0。
返回值:成功返回0;错误返回-1
cmd:有三个
IPC_RMID:常用做删除共享内存
IPC_STAT:得到共享内存的状态,把共享内存的shmid_ds结构复制到buf中;
IPC_SET:改变共享内存的状态,把buf所指的shmid_ds结构中的uid、gid、mode复制到共享内存的shmid_ds结构内。(内核为每个共享存储段维护着一个结构,结构名为shmid_ds,这里就不讲啦,里面存放着共享内存的大小,pid,存放时间等一些参数)
例如:int ret = shmctl(id, IPC_RMID,NULL);删除id号的共享存储空间
ps:在Linux下,比如你申请24字节大小的共享存储空间,系统还是会默认给你分配一页的大小,但你还是只能使用这一页上24字节的空间。使用getconf PAGE_SIZE 命令就能显示出一页的大小
使用ipcs -m可以查看当前系统所有的共享内存空间信息
如果你的程序创建了一个共享内存段,但没有销毁,可以使用命令ipcrm -m shmid命令删除共享内存段,不然程序再运行有可能出错。
//c10.c -o -w 写入
#include<stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/shm.h>
//删除一般在 读端
int main(){
key_t key=ftok(".",1);
int shmid=shmget(key,1024*3,IPC_CREAT|0666);
char *address;
address=shmat(shmid,NULL,0);//address=shmat(shmid,0,0);
strcpy(address,"Hello!");
//address="Hello!";错误
printf("write OK\n");
shmdt(address);
return 0;
}
//gcc c11.c -o -r 读端
#include<stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/shm.h>
//读端删除内存
int main(){
key_t key=ftok(".",1);
int shmid=shmget(key,1024*3,IPC_CREAT|0666);
//int shmid=shmget(key,1024*3,0); 读端可以不用权限
char *addre;
addre=shmat(shmid,NULL,0);
printf("%s\n",addre);
//解除映射
shmdt(addre);
//删除
shmctl(shmid,IPC_RMID,NULL);
printf("read OK\n");
return 0;
}
5 信号(Signal)
- 信号是Linux系统中用于进程间互相通信或者操作的一种机制,信号可以在任何时候发给某一进程,而无需知道该进程的状态。
- 如果该进程当前并未处于执行状态,则该信号就有内核保存起来,知道该进程回复执行并传递给它为止。
- 如果一个信号被进程设置为阻塞,则该信号的传递被延迟,直到其阻塞被取消是才被传递给进程。
Linux系统中常用信号:
(1)SIGHUP:用户从终端注销,所有已启动进程都将收到该进程。系统缺省状态下对该信号的处理是终止进程。
(2)SIGINT:程序终止信号。程序运行过程中,按Ctrl+C
键将产生该信号。
(3)SIGQUIT:程序退出信号。程序运行过程中,按Ctrl+\\
键将产生该信号。
(4)SIGBUS和SIGSEGV:进程访问非法地址。
(5)SIGFPE:运算中出现致命错误,如除零操作、数据溢出等。
(6)SIGKILL:用户终止进程执行信号。shell下执行kill -9
发送该信号。
(7)SIGTERM:结束进程信号。shell下执行kill 进程pid
发送该信号。
(8)SIGALRM:定时器信号。
(9)SIGCLD:子进程退出信号。如果其父进程没有忽略该信号也没有处理该信号,则子进程退出后将形成僵尸进程。
信号来源
信号是软件层次上对中断机制的一种模拟,是一种异步通信方式,,信号可以在用户空间进程和内核之间直接交互,内核可以利用信号来通知用户空间的进程发生了哪些系统事件,信号事件主要有两个来源:
- 硬件来源:用户按键输入
Ctrl+C
退出、硬件异常如无效的存储访问等。 - 软件终止:终止进程信号、其他进程调用kill函数、软件异常产生信号。
信号生命周期和处理流程
(1)信号被某个进程产生,并设置此信号传递的对象(一般为对应进程的pid),然后传递给操作系统;
(2)操作系统根据接收进程的设置(是否阻塞)而选择性的发送给接收者,如果接收者阻塞该信号(且该信号是可以阻塞的),操作系统将暂时保留该信号,而不传递,直到该进程解除了对此信号的阻塞(如果对应进程已经退出,则丢弃此信号),如果对应进程没有阻塞,操作系统将传递此信号。
(3)目的进程接收到此信号后,将根据当前进程对此信号设置的预处理方式,暂时终止当前代码的执行,保护上下文(主要包括临时寄存器数据,当前程序位置以及当前CPU的状态)、转而执行中断服务程序,执行完成后在回复到中断的位置。当然,对于抢占式内核,在中断返回时还将引发新的调度。
信号的生命周期
相关函数:
kill -l 查看所有信号 KILL -L
kill -9 -pid 杀死pid进程
信号处理方式
Linux的每一个信号都有一个缺省的动作,典型的缺省动作是终止进程,当一个信号到来的时候收到这个信号的进程会根据信号的具体情况提供一下三种不同的处理方式:
1.捕捉:·对于需要处理的信号,指定处理函数,由内核调用该函数来处理。
· 2.忽略某个信号,对该信号不做任何处理。
· 3.对该信号的处理保留系统的默认值,这种缺省操作大多数使得进程终止,进程通过系统调用signal函数来指定进程对某个信号的处理行为
Linux信号的使用方法
注册信号
指的是在目标进程中注册,该目标进程中有未决信号的信息。对于已经有自己的中断处理例程的信号而言,其注册就是一个用户进程自己定义的处理函数去替换Linux内核预定义的函数动作。 例如“Ctrl+C”是中止当前进程的运行,按下组合键后会产生一个中断,当前进程会接收到一个SIGINT信号,然后找到对应的处理例程是终止当前进程。我们也可以自己注册信号,将SIGINT信号对应的处理例程改为我们想要的操作。
struct sigpending{
struct sigqueue *head, **tail;
sigset_t signal;
};
struct sigqueue{
struct sigqueue *next;
siginfo_t info;
};
其中 sigqueue结构组成的链称之为未决信号链,sigset_t称之为未决信号集,*head,**tail分别指向未决信号链的头部与尾部,siginfo_t info是信号所携带的信息。
信号注册的过程就是将信号值加入到未决信号集siginfo_t中,将信号所携带的信息加入到未决信号链的某一个sigqueue中去。因此,对于可靠的信号,可能存在多个未决信号的sigqueue结构,对于每次信号到来都会注册。而不可靠信号只注册一次,只有一个sigqueue结构。只要信号在进程的未决信号集中,表明进程已经知道这些信号了,还没来得及处理,或者是这些信号被阻塞。
在Linux中,可以通过signal和sigaction函数注册信号并指定接收到该信号时需要完成的动作,对于已经有自己的功能动作的信号而言其注册就是用一个用户自己定义的动作去替换Linux内核预定义的动作。
signal函数
设置某一信号的对应动作。如果正在运行的程序源代码里注册了针对某一信号的处理程序,不论当时程序执行到何处,一旦进程接收到该信号,相应的调用就会发生。
#include <signal.h>
//把函数指针sighandler_t定义成一种类型。可写可不写 void handler(int signum){}
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
//返回先前的信号处理函数指针,错误则返回SIG_ERR(-1)。
signum:指明了所要处理的信号类型,它可以取除了SIGKILL和SIGSTOP外的任何一种信号。
函数:handler:描述了与信号关联的动作,它可以取以下三种值:
· 1. SIG_IGN:忽略signum指出的信号。
· 2.SIG_DFL:恢复对信号的系统默认处理。不写此处理函数默认也是执行系统默认操作。
3. 自定义一个函数,执行这个函数
void handler (int signum){//传递的唯一参数 int sigum
//你想做的处理
}
int main(){
signal(SIGINT,handker);
}
当signal函数调用成功后返回信号以前的处理配置,如果调用失败则返回SIG_ERR(-1)。需要注意的是并非程序执行到signal调用时就立即对指定的信号进行操作,因为信号的产生是无法预期的,利用signal设置信号处理函数只是告诉系统对这个信号用什么程序来处理。
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <signal.h>
//可写可不写 声明是一种函数指针,这种指针指向的函数接受一个整型参数并返回一个无类型指针
//typedef void (*signal_handler)(int);
void fun(int signum){
printf(" no exit sigum=%d\n",signum);
}
int main(){
signal(SIGINT,fun); //为指针不需要添加参数
while(1);
return 0;
}
sigaction函数
相较于signal函数,sigaction函数在完成信号注册工作的同时提供了更多功能选择。
#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
//signum指定要处理的信号
//act 包含了信息,处理函数等
//oldact备份了原信息、原来的处理函数,,,,一般可以为NULL
其中,struct sigaction结构定义如下:
//信号的一些配置、包括了处理函数
struct sigaction {
//信号处理函数指针、指向信号处理函数的地址。与signal的第二参数一样。SIG_IGN/SIG_DFL/fun
void (*sa_handler)(int);
//指向函数的指针、信号处理函数
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask; //默认处理信号时阻塞
int sa_flags;
};
//sa_sigaction 、sa_flags需要配置
//sa_handler 不接受数据 sa_mask 默认
它指向的函数有三个参数,其中第二个为siginfo_t结构体,这个结构体包含了一些进程的信息定义如下:
struct siginfo_t {
int si_signo; //Signal number
int si_errno; //Errno value
int si_code; //Signal code
pid_t si_pid; //Sending process ID
uid_t si_uid; //Real user ID of sending process
int si_status; //Exit value or signal
clock_t si_utime; //User time consumed
clock_t si_stime; //System time consumed
sigval_t si_value; //Sigval value 想获取的数据
int si_int; //POSIX.1b signal 想获取的数据
void *si_ptr; //POSIX1.b signal
void *si_addr; //Memory location that caused fault
int si_band; //Band event
int si_fd; //File descriptor
};
typedef union sigval {
int sival_int;
void *sival_ptr;
}sigval_t;
第三个参数void *: void (*sa_sigaction)(int, siginfo_t *, void *);
为空:消息有数据 非空:无数据
sa_flags用于指示信号处理函数的不同选项,可以通过“|”连接不同的参数,从而实现所需的选项设置。具体的可选参数如下表所示:
sa_flags可选项及对应设置
sa_flags | 对应设置 |
SA_NOCLDSTOP | 用于指定信号SIGCHLD,当子进程被中断时,不产生此信号,当且仅当子进程结束时产生该信号 |
SA_NOCLDWAIT | 当信号为SIGCHLD时,此选项可以避免子进程的僵死 |
SA_NODEFER | 当信号处理程序正在运行时,不阻塞信号处理函数自身的信号功能 |
SA_NOMASK | 同SA_NODEFER |
SA_ONESHOT | 当用户注册的信号处理函数被调用过一次之后,该信号的处理程序恢复为缺省的处理函数 |
SA_RESETHAND | 同SA_ONESHOT |
SA_RESTART | 使本来不能进行自动重新运行的系统调用自动重新启动 |
SA_SIGINFO | 表明信号处理函数是由sa_sigaction指定,而不是由sa_handler指定,它将显示更多处理函数的信息 |
实例如下:
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
void signalDeal(int sig, siginfo_t *info, void *t) {
if (sig == SIGINT) {
printf("Ctrl+C被按下\n");
}
else if (sig == SIGQUIT) {
printf("Ctrl+/被按下\n");
}
else {
printf("其他信号\n");
}
}
int main(int argc, char *argv[])
{
struct sigaction act;
act.sa_sigacion = signalDeal;//信号处理函数
sigempty(&act.sa_mask);
act.sa_flags = SA_SIGINFO;
sigaction(SIGINT, &act, NULL);
sigaction(SIGQUIT, &act, NULL);
while (1) {}
return 0;
}
发送信号
kill函数
kill函数将信号发送给进程或者进程组。
#include <signal.h>
#include <sys/types.h>
int kill(pid_t pid, int sig);
其中pid参数的取值如下:
pid参数取值及其定义
pid | 含义 |
pid > 0 | 将信号发送给进程号为pid的进程 |
pid = 0 | 将信号发送给与目前进程相同进程组的所有进程 |
pid < 0 && pid != -1 | 向进程组ID为pid绝对值的进程组中的所有进程发送信号 |
pid = -1 | 除发送给自身进程外,还向所有进程ID大于1的进程发送信号 |
例如:sig:signum
sig参数对应的是信号编码,当其为0(即空信号)时,实际不发送任何信号,但照常进行错误检查。因此可用于检查目标进程是否存在,以及当前进程是否具有向目标发送信号的权限。
如果采用外部传参发送信号 , int argc,char **argv 传参过程中 kill -9 pid 里面9和pid为整型数。这里为char 需要更改类型,更改类型函数 atoi(argv[1])
//fun 取代SIGINT信号功能 ctrl +c 无效 gcc c13.c ./a.out 当前进程有效
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <signal.h>
//typedef void (*signal_handler)(int);
void fun(int signum){
printf(" no exit sigum=%d\n",signum);
}
int main(){
signal(SIGINT,fun);
while(1);
return 0;
}
//发送信号,结束上面的a.out
// 查询pid ps -aux|grep a.out 发送信号./kill 9 6922 9杀死进程
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
//typedef void (*signal_handler)(int);
int main(int argc ,char **argv)
{
int signum;
pid_t pid;
//char 更改为 int 类型
signum=atoi(argv[1]);
pid=atoi(argv[2]);
//发送消息第一种
kill(pid,signum);
//第二种
char cmd[128];
sprintf(cmd,"kill %d %d",signum,pid); //将kill %d %d 存放在cmd中
system(cmd);
printf("num=%d,pid=%d\n",signum,pid);
return 0;
}
raise函数
raise函数向自身所在进程发送一个信号。
#include <signal.h>
int raise(int sig);
sigqueue函数
sigqueue主要是针对实时信号提出的(当然也支持非实时信号)信号发送函数,通常与函数sigaction配合使用。
#include <signal.h>
int sigqueue(pid_t pid, int sig, const union sigval value);
// 进程号 发的什么信号 消息
typedef union sigval {
int sival_int;
void *sival_ptr; //发送字符串需要用共享内存传递
}sigval_t;
sigqueue比kill传递了更多的附加信息,但sigqueue只能向一个进程发送信号,不能发送信号给一个进程组。
//例如:发送 gcc c14.c -o sig_send ps -aux|grep sig_receive ./sig_send 9 8046
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
int main(int argc ,char **argv)
{
int signum;
pid_t pid;
signum=atoi(argv[1]);
pid=atoi(argv[2]);
union sigval value;
value.sival_int=100;
sigqueue(pid,SIGINT,value);
printf("send to %d succeed\n",pid);
return 0;
}
//接受信号 gcc c15.c -o sig_receive ./sig_receive
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
void handler(int signum, siginfo_t *infor,void *off){ //处理函数
if(signum==SIGINT)
printf("signum=%d,but no exit\n",signum);
if(off!=NULL){
printf("get date from %d :%d \n",infor->si_pid,infor->si_int);
printf("you can receive %d\n",infor->si_value.sival_int);//易错点
}
}
int main(int argc ,char **argv)
{
struct sigaction informaction;
informaction.sa_sigaction=handler; //配置信号处理
informaction.sa_flags=SA_SIGINFO;
sigaction(SIGINT,&informaction,NULL);
while(1);
return 0;
}
Linux信号集
在Linux系统的实际应用中,常常需要将多个信号组合起来使用,这种用来表示多个信号的数据类型被称为信号集(signal set),其定义格式为sigset_t。
#include <signal.h>
typedef struct {
unsigned long sig[_NSIG_WORDS];
}sigset_t;
有5个函数用于信号集的操作:
#include <signal.h>
//前4个函数若调用成功则返回0,否则返回-1
int sigemptyset(sigset_t *set); //用于将参数set所指向的信号集设为空,即不包含任何信号
int sigfillset(sigset_t *set); //用于将参数set所指向的信号集设定为满,即包含所有的信号
int sigaddset(sigset_t *set, int signum); //用于将参数signum所代表的信号添加到参数set所指向的信号集中
int sigdelset(sigset_t *set, int signum); //用于将参数signum所代表的信号从参数set所指向的信号集中删除
int sigismember(const sigset_t *set, int signum); //用于检查参数signum是否位于参数set所指向的信号集中,如果为真则返回1,为假则返回0,出错则返回-1
信号的阻塞和挂起
在实际应用中,有时候既不希望进程在接收到信号时立刻中断,也不希望该信号完全被忽略,而是希望进程延迟处理。这可以通过阻塞信号的方法来实现。Linux提供了sigprocmask函数和和sigsuspend函数用于信号的阻塞和挂起。
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
int sigsuspend(const sigset_t *mask);
函数sigprocmask的参数set、oldset指向信号集。set指向一个信号集时,参数how表示sigprocmask函数如何对set所指向的信号集以及信号掩码进行操作。当参数set的值为NULL时,how的取值无效。当oldset不为NULL时,函数将进程当前的信号掩码写入oldset指向的信号集。
参数how的取值及对应功能
how | 对应函数功能 |
SIG_BLOCK | 将set所指向的信号集中所包含的信号加到当前的信号掩码中,即信号掩码与set信号集做逻辑或运算 |
SIG_UNBLOCK | 将set所指向的信号集中所包含的信号从当前的信号掩码中删除 |
SIG_SETMASK | 设定新的当前信号掩码为set所指向的信号集中所包含的信号 |
函数sigsuspend的作用是挂起信号。在调用该函数后,进程停止执行,等待着开放信号的唤醒。