Linux系统编程(3)

Linux系统编程(3)——进程间通信


前言

这算是我自己的一个Linux系统编程学习路上的一个学习笔记,学习的过程中看过一些视频+博客,所以在学习过后根据记录的笔记来完成代码实现的过程中,可能会出现一大段文章内容和别人写的一样或者某些思想也会相同,如有侵权,请联系删除或者添加引用。(本文章不会作为商业用途)

一、管道

管道仅用于两个有血缘关系的的进程,是一个伪文件,不会占用磁盘空间。
原理:内核使用环形队列机制,借助内核缓冲区(4K)实现。
局限性: 1.数据不能自己写自己读;2.管道数据不可反复读取;3.采用半双工通信方式,数据只能在单方向上流动;4.只能在有公共祖先的线程间使用。

int fd[2];   //创建管道,读写两个端口
pipe(fd);    //把fd[2]初始化为线程能够识别的管道
//fd[0]端为读端,使用read()函数读取就行。
//fd[1]端为写端,使用write()函数向里面写数据就好。

ulimit -a;  //终端命令,查看管道的大小

long fpathconf(int fd, int name);
//查看fd文件中name参数的长度

此外,还有另一种 FIFO 的管道,被称为命名管道,没有血缘关系(不相关)的进程也可以交换数据。

mkfifo 管道名;   //使用命令创建管道
int mkfifo(const char *pathname,mode_t mode);//使用函数创建管道
建立的管道是个文件,可以推广到各进程对同一文件进行操作。此时对管道的操作和之前对文件的操作一模一样。

二、共享映射区——存储映射I/O

void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
//创建共享内存映射区
//addr:指定映射区的首地址,通常传NULL,让系统自动分配。
//length:共享内存映射区的大小。单位:字节
//prot:共享内存映射区的读写属性。PROT_NONE:无权限,基本没有用;PROT_READ:读权限;PROT_WRITE:写权限;     PROT_EXEC:执行权限。需要哪些权限或起来就行了。
//flags:共享内存映射区的共享属性。
//1. MAP_FIXED:开启这个选项,则 addr 参数指定的地址是作为必须而不是建议。如果由于空间不足等问题无法映射则调用失败。不建议使用。
//2. MAP_PRIVATE:表明这个映射不是共享的。文件使用 copy on write 机制映射,任何内存中的改动并不反映到文件之中。也不反映到其他映射了这个文件的进程之中。如果只需要读取某个文件而不改变文件内容,可以使用这种模式。
//3.MAP_SHARED:和其他进程共享这个文件。往内存中写入相当于往文件中写入。会影响映射了这个文件的其他进程。与 MAP_PRIVATE冲突。
//fd:文件描述符。进行 map 之后,文件的引用计数会增加。
//因此,我们可以在 map 结束后关闭 fd,进程仍然可以访问它。当我们 unmap 或者结束进程,引用计数会减少。
//offset:偏移位置,必须时4k的整数倍。默认为0,表示映射文件全部,即不偏移。
//返回值:成功返回映射区的首地址;失败返回MAP_FAILED(失败值-1的地址),且设置errno。

使用mmap()的注意事项:

  1. 用于创建映射区文件的大小为0,实际指定非0大小创建映射区,出总线错误。
  2. 用于创建映射区文件的大小为0,实际指定非0大小创建映射区,出无效参数错误。
  3. 文件读写属性为只读,映射区出现读写,出无效参数错误。
  4. 创建映射区需要read属性,mmap的读写权限应小于等于open打开的fd权限,只写也不行。
  5. 文件描述符fd在mmap创建映射区完成时可以关闭,后面用映射区地址指针对它进行操作。
  6. offset必须是4096的整数倍数,因为MMU映射的最小单位是4k。
  7. 对申请的内存不能越界访问。
  8. mmap用于释放的地址必须是申请返回的地址。
  9. 如果映射区的访问权限设置为私有,对内存所做的所有修改,只会在内存有效,不会反应到磁盘文件中。
  10. 如果映射区的访问权限设置为私有,只需要open文件时有读权限,用于创建映射区即可。
mmap()创建映射区实例:
struct student
{
        int id;
        char name[256];
        int age;
};
1.写数据进程
int fd = open("./test01.txt", O_RDWR | O_CREAT | O_TRUNC, 0644);
struct student stu = {1,"ZhangSan",20};
ftruncate(fd, sizeof(stu));
struct student* p = (struct student*)mmap(NULL, sizeof(stu), PROT_WRITE, MAP_SHARED, fd, 0);
//把test01.txt文件映射到 *p 所指向的内存映射区中。
//后面对内存映射区的读写实际上就是对文件描述符fd的读写了。
close(fd); //关闭文件描述符。
while(1)
{
    cout<<"Sname="<<stu.name<<" Sid="<<stu.id<<" Sage="<<stu.age<<endl;
    memcpy(p, &stu, sizeof(stu));  //对申请的映射区进行写操作
    cout<<"WPname="<<p->name<<" WPid="<<p->id<<" WPage="<<p->age<<endl;
    stu.id++;
    sleep(1);
}
munmap(p,sizeof(stu));//释放内存映射区

2.读数据进程:
int fd = open("./test01.txt", O_RDONLY);
struct student stu,*p;
p = (struct student*)mmap(NULL, sizeof(stu), PROT_READ, MAP_SHARED, fd, 0);
//在映射的时候其实已经把fd表示的文件的内容读取到了映射区地址指针中。
close(fd);
while(1)
{
    cout<<"  RPid="<<p->id<<" RPneme="<<p->name<<" RPage="<<p->age<<endl;
    //直接访问数据即可
    sleep(1);
}
munmap(p,sizeof(stu));

mmap匿名映射区: mmap() 函数参数 flags 设置为 MAP_SHARED | MAP_ANONYMOUS 。但是此时的映射区只能用于有血缘关系的进程间通信,没有血缘关系的则不行。

如果系统不支持 MAP_ANONYMOUS ,可以在前方把
fd = open("/dev/zero", …) ,达到同样的效果。因为/dev/zero 文件表示你可以取任意大小的空间。
/dev/null:黑洞文件,无论你写多少内容到该文件下,它都不会满。

三、信号

原理: 信号是软件层面上的“中断”,一旦信号产生,无论程序执行到什么位置,必须停止运行,转向信号处理,处理结束了再继续执行后续程序。所有信号的产生和处理都是由内核完成的。

信号的产生: 自行百度。

信号的处理方式: 1.执行默认动作;2.忽略(丢弃);3.捕捉(调用用户处理函数)

信号屏蔽字: 也称阻塞信号集。将某些信号加入集合,就可以对他们设置屏蔽,当屏蔽信号后,再收到该信号,该信号的处理将会后推,后推到解除屏蔽后。

未决信号集: 1.当信号产生时,未决信号集中代表该信号的位立马翻转为1,表示信号处于未决状态,当信号被处理后,对应的信号位立马翻转为0。这个过程往往非常短暂。 2.信号产生后由于某些原因(主要是阻塞)不能抵达,这类信号的集合称之为未决信号集,在某些原因解决前,信号一直处于未决状态。

常规信号: kill -l 命令后出现的一系列信号值中(1)~(31)号为常规信号,(34) ~ (64)称为实时信号。每个常规信号都包含四要素:1.编号;2.名称;3.事件(触发该信号的条件称为事件);4.默认处理动作。此处可以自行百度各个信号的四要素,或者英语好可以使用 man 7 signal 自己查看。

unsigned int alarm(unsigned int seconds);
//产生闹钟信号 SIGALRM 。每个进程有且只有唯一一个定时器
//seconds:定时器设置的定时秒数,时间到,给进程发送SIGALRM信号
//alarm(0)取消定时器,返回旧闹钟余下的秒数。

time ./可执行文件  
//该终端命令用来查看程序执行时间
//real:程序执行实际用时
//user:程序在用户空间运行的时间
//sys:程序在内核空间运行的时间
//real = user + sys
//优化代码首先考虑有优化I/O,因为使用硬件比较耗时,浪费资源。

int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);
//用来创建定时器, 不过它比alarm功能多, 最显著的区别就是它可以指定到微秒, 而且可以循环发送
//which:定时器类型,如下 3 种
//ITIMER_REAL : 以系统真实的时间来计算,它送出SIGALRM信号。相当于 alarm 函数  
//ITIMER_VIRTUAL : 以该进程在用户态下花费的时间来计算,它送出SIGVTALRM信号。  
//ITIMER_PROF : 以该进程在用户态下和内核态下所费的时间来计算,它送出SIGPROF信号。
//itimerval *new_value :
struct itimerval {
     struct timeval it_interval; // 间隔时间,用于循环。第一次闹钟响了后,每隔多长时间闹钟再次响起。
     struct timeval it_value; //初始定时时间。就像早上起床闹钟第一次响的时间
};
struct timeval {
    long tv_sec; //定时时间中的秒
    long tv_usec; 定时时间中的微秒
};
//itimerval *old_value:传出参数,传出上次定时剩余时间。

int kill(pid_t pid, int sig);
//向进程id为 pid 的进程发送 sig 信号(kill -l 查出来的所有信号都可以)
//pid > 0:向指定进程发送sig
//pid = 0:向和kill()函数处于同一进程组的进程发送sig
//pid < -1:向以pid绝对值为组标识的进程发送sig
//pid = -1;向本进程有权限发送信号的所有进程发送sig
//root用户可以法信号给所有用户,但是普通用户只能向自己创建的进程发送信号。

SIGCHILD 信号:
//只要子进程的状态发生变化,就会产生SIGCHILD信号。多用于:
//1.子进程终止时
//2.子进程收到SIGSTOP信号暂停时
//3.子进程处于暂停状态,收到SIGCONT后唤醒时
//所以可以借助SIGCHILD信号回收子进程,回收子进程只和父进程有关

信号集操作函数
1.信号集设定: 因为阻塞信号集和未决信号集都在进程控制块PCB内,不方便直接拿出来使用,于是我们在外部先定义一个同长度的数据,最后替换到 PCB 控制块中即可。

sigset_t set; //外部定义一个sig标志的数据
int sigemptyset(sigset_t *set);
//把sig标志位清空
int sigfillset(sigset_t *set);
//把sig标志位所有位设置为1,也即把所有信号加入集合
int sigaddset(sigset_t *set, int signum);
//将指定的信号加入到信号集里:signum 可以通过kill -l 指令查看。
int sigdelset(sigset_t *set, int signum);
//将指定的信号从信号集里删除
int sigismember(const sigset_t *set, int signum);
//查看指定的信号是否在信号集里
//上面所以函数的返回值相同:成功返回0,失败返回-1。

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
//屏蔽信号或者解除屏蔽。本质是读取或者修改进程的信号屏蔽字
//how:如何设置,三种方式,如下:
//SIG_BLOCK:将set所指向的信号集中包含的信号加到当前的信号掩码中。即信号掩码和set信号集进行或操作。mask = mask | set
//SIG_UNBLOCK:将set所指向的信号集中包含的信号从当前的信号掩码中删除。即信号掩码和set取反值进行与操作。mask = mask | ~set
//SIG_SETMASK:将set的值设定为新的进程信号掩码。即set对信号掩码进行了赋值操作,mask = set。不推荐此用法,因为它是批量处理。

int sigpending(sigset_t *set);
//获取当前进程的未决信号集。
//set:传出参数,表示未决信号集
//返回值:成功返回0;失败返回-1。

2.信号捕捉函数:

typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
//signal 注册一个信号捕捉函数,而不是signal函数来捕捉,是系统来捕捉。
//signum:需要捕捉的信号。
//handler:为捕捉到信号后执行操作的函数,自己设置的,这样子就不用该信号的默认处理方式了。

 int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
 struct sigaction {
    void   (*sa_handler)(int); //和signal的handler相同,信号捕捉后的处理函数
    void   (*sa_sigaction)(int, siginfo_t *, void *);
    //另一个信号处理函数,它有三个参数,可以获得关于信号的更详细的信息。
    //当 sa_flags 成员的值包含了 SA_SIGINFO 标志时,系统将使用 sa_sigaction 函数作为信号处理函数,否则使用 sa_handler 作为信号处理函数。
    sigset_t   sa_mask;  //设置在处理该信号时暂时将sa_mask 指定的信号集搁置,只作用在捕捉函数工作期间
    int    sa_flags;
    //sa_flags 用来设置信号处理的其他相关操作,下列的数值可用:
    //SA_RESETHAND:当调用信号处理函数时,将信号的处理函数重置为缺省值SIG_DFL
	//SA_RESTART:如果信号中断了进程的某个系统调用,则系统自动启动该系统调用
	//SA_NODEFER :一般情况下, 当信号处理函数运行时,内核将阻塞该给定信号。但是如果设置了 SA_NODEFER标记, 那么在该信号处理函数运行时,内核将不会阻塞该信号。
	//一般使用 sa_flags = 0,表示屏蔽当前已经捕捉到的函数
    void   (*sa_restorer)(void); //是一个已经废弃的数据域,不要使用。
};
//act:指定新的信号处理方式
//oldact:用来保存先前信号的处理方式(如果不为NULL的话);如果不想保存,传 NULL 即可

信号捕捉特性:
1.进程正常进行时,默认的PCB中有一个信号屏蔽字,假定为▲,它决定了当前进程自动屏蔽哪些信号。当注册了某个信号的捕捉函数之后,该函数执行期间所屏蔽的信号不由▲来指定,而是用sa_mask来指定,调用完信号处理函数后,才恢复▲的管理地位。
2.某个信号的捕捉函数执行期间,该信号自动被屏蔽。
3.阻塞的常规信号不支持排队,多次产生但只记录一次。后32个实时信号支持排队。
4.如果不屏蔽其他信号,在捕捉函数执行期间,其他信号能够递达。

四、本地套接字

进程间本地套接字的通信方式类似于客户端和服务器间的网络通信。

本地套接字步骤(“服务器”端)
1.int socket(int domain, int type, int protocol);
//domain:协议族,对于本地套接字来说,其值为AF_UNIX 或者 AF_LOCAL
//type:套接字类型,可设置为SOCK_STREAM(流式套接字)活SOCK_DGRAM(数据报式套接字),此处不存在TCP和UDP之分
//protocol:指定具体的协议,应被设置为0
2.int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
//绑定地址结构(此处的地址结构为 sockaddr_un,存放“服务器的文件路径)
struct sockaddr_un{
  sa_family_t    sun_family;        // AF_UNIX or AF_LOCAL,和socket中保持一致
  char    sun_path[UNIX_PATH_MAX];  // 路径名。
};
struct sockaddr_un serv_addr;
serv_addr.sun_family = AF_UNIX or AF_LOCAL;
unlink("server.socket");
strcpy(serv_addr.sun_path,"server.socket");
socklen_t len = offsetof(sockaddr_un,sun_path);
bind(lfd, (sockaddr *)&serv_addr, len);

3.int listen(int sockfd, int backlog);

4. int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
5. //等待连接,通过连接后返回的套接字,即可完成同对端的通信。
本地套接字步骤(“客户”端)
1.int socket(int domain, int type, int protocol);
//和网络通信一样,使用socket创建出来的套接字,来和服务器端进行通信

2.struct sockaddr_un serv_addr;
//把服务器的相关信息存放到里面

3.int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
//sockfd:socke函数返回的值直接使用
//addr:对端地址结构,此处为第二步创建的服务器地址结构
//addrlen:对端地址结构长度
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值