Linux驱动基础

1、Linux虚拟地址空间是如何分布的?

  • 每个进程都有独立的虚拟地址空间,进程访问的虚拟地址并不是真正的物理地址;
  • 虚拟地址可通过每个进程上的页表(在每个进程的内核虚拟地址空间)与物理地址进行映射,获得真正物理地址;
  • 如果虚拟地址对应的物理地址在物理内存中不存在,则产生缺页中断去真正地分配物理地址,同时更新进程中的页表;如果此时物理内存已经耗尽,则根据内存替换算法淘汰部分页面,把这些页面的内容从物理内存写回到物理磁盘中,并释放这些页面对应的物理内存。

Linux使用虚拟地址空间,大大提高了进程的寻址能力,由低地址到高地址分别为:

  1. 只读段:该部分空间只能读,不可以写(包括:.text代码段、.rodata[Read-Only Data]段(C字符串常量、#define宏定义的常量和const常量)),通常程序加载运行时,.text和.rodata统称为.text段;
  2. 数据段:该部分空间可读可写,保存全局变量、静态变量(.data数据段用来存放已初始化并且初始化不为0的全局变量和静态变量)(.bss[Block Started by Symbol]段用来存放未初始化或初始化为0的全局变量和静态变量);
  3. :就是平常所说的动态内存,malloc/new大部分来源于此,其中堆顶的位置可通过函数brk()或sbrk()进行动态调整;
  4. 文件映射区:如动态库、共享内存等映射物理空间的内存,一般是mmap函数所分配的虚拟地址空间;
  5. :用于维护函数调用的上下文空间,一般为8M,可通过ulimit -s查看;
  6. 内核虚拟空间:用户代码不可见的内存区域,由内核管理(页表就存在内核虚拟空间)。
    下图是32位系统典型的虚拟地址空间分布:
    在这里插入图片描述

32位的Linux系统虚拟地址空间为4G,这4G的空间被分为两部分,最高的1G空间(从虚拟地址0xC0000000 ~ 0xFFFFFFFF)供内核使用,也即内核空间;较低的3G空间(从虚拟地址0x00000000 ~ 0xBFFFFFFF)供各个进程使用,也即用户空间。因为每个进程可以通过系统调用进入内核,因此,Linux内核由系统内的所有进程共享。于是,从进程的角度来看,每个进程可以拥有4G的虚拟空间。

2、mmap底层实现原理?

(1)mmap内存映射
在unix/linux平台下读写文件,一般有两种方式。第一种是首先open文件,接着使用read系统调用函数来读取文件。于是内核将文件的内容从磁盘上读取到内核页高速缓存,再从内核高速缓存读取到用户进程的地址空间。这么做需要在内核和用户空间之间做多次数据拷贝。而且当多个进程同时读取一个文件时,则每一个进程在自己的地址空间都有这个文件的副本,这样也造成了物理内存的浪费。如下图所示:
在这里插入图片描述
第二种方式是使用内存映射。内存映射,简而言之就是将用户空间的一段内存区域映射到内核空间,映射成功后,用户对这段内存去区域的修改可以直接反映到内核空间,同样,内核空间对这段区域的修改也直接反映到用户空间。那么对于内核空间<---->用户空间两者之间需要大量数据传输等操作的话效率是非常高的。mmap是一种内存映射文件的方法,将一个文件或者其他对象映射到进程的地址空间,实现文件磁盘和进程虚拟空间中一段虚拟地址的一一对应关系。实现这样的映射关系后,进程就可以采用指针的方式读写操作这一段虚拟内存,而系统会自动回写脏页面到对应的文件磁盘上,即完成了对文件的操作而不必再调用readwrite等系统调用函数。相反,内核空间对这段区域的修改也直接反应到用户空间,从而可以实现不同进程间的文件共享。如下图所示:
在这里插入图片描述
从上图可以看出,进程的虚拟地址空间由多个虚拟内存区域构成。虚拟内存区域是进程的虚拟地址空间中的一个同质区间,即具有同样特性的连续地址范围。上图中所示的text数据段(代码段)、初始数据段、BSS数据段、堆、栈和内存映射,都是一个独立的虚拟内存区域。而为内存映射服务的地址空间处在堆栈之间的空余部分。
linux内核使用vm_area_struct结构体来表示一个独立的虚拟内存区域,由于每个不同质的虚拟内存区域功能和内部机制都不同,因此一个进程使用多个vm_area_struct结构体来分别表示不同类型的虚拟内存区域。各个vm_area_struct结构体使用链表或者树形结构链接,方便进程快速访问,如下图所示:
在这里插入图片描述
vm_area_struct结构体中包含区域起始和终止地址以及其他相关信息,同时也包含一个vm_ops指针,其内部可引出针对这个区域支持使用的所有系统调用函数。这样,进程对某一虚拟内存区域的任何操作需要用到的信息都可以从vm_area_struct中获得。mmap函数就是要创建一个新的vm_area_struct结构体,并将其与文件的物理磁盘地址相连。
(2)mmap内存映射实现过程
mmap内存映射的实现过程总体上可以分为三个阶段:

  • 进程启动映射过程,并在虚拟地址空间中为映射创建虚拟映射区域
    [1] 进程在用户空间调用库函数mmap(开发者调用的mmap),原型:void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);
    [2] 在当前进程的虚拟地址空间中,寻找一段空闲的满足要求的连续的虚拟地址;
    [3] 为此虚拟空间分配一个vm_area_struct结构体,接着对这个结构体的各个字段进行初始化;
    [4] 把新建的虚拟区结构体(vm_area_struct)插入进程的虚拟地址区域链表或树中。

  • 调用内核空间的系统调用函数mmap(不同于用户空间的库函数),实现文件物理地址和进程虚拟地址的一一映射关系
    [5] 为映射分配了新的虚拟地址区域后,通过待映射的文件指针,在文件描述符表中找到对应的文件描述符,通过文件描述符链接到内核“已打开文件集”中该文件的文件结构(struct file),每个文件结构体维护着和这个已打开文件相关各项信息。
    [6] 通过该文件的文件结构体,链接到file_operations模块,调用内核函数mmap,其原型为:int mmap(struct file *file, struct vm_area_struct *vma),不同于用户空间函数。
    [7] 内核mmap函数通过虚拟文件系统inode模块定位到文件磁盘物理地址。
    [8] 通过remap_pfn_range函数建立页表,即实现了文件地址和虚拟地址区域的映射关系。此时,这片虚拟地址并没有任何数据关联到物理内存中。

  • 进程发起对这片映射空间的访问,引发缺页中断,实现文件内容到物理内存的拷贝

    注:前两个阶段仅在于创建虚拟区域并完成映射,但是并没有将任何的文件数据拷贝到物理内存中。真正的文件读取是当进程发起读或写操作时。

    [9] 进程的读或写操作访问虚拟地址空间这一段映射地址,通过查询页表,发现这一段地址并不在物理页面上。因为目前只建立了地址映射,真正的硬盘数据还没有拷贝到物理内存中,因此引发缺页中断。
    [10] 缺页中断进行一系列判断,确定无非法操作后,内核发起请求调页过程。
    [11] 调页过程先在交换缓存空间中寻找需要访问的物理内存页,如果没有则调用nopage函数把所缺的页从磁盘装入到物理内存中。
    [12] 之后进程即可对这段内存进行读或写的操作,如果写操作改变了其内容,一定时间后系统会自动回写脏页面到对应磁盘地址,也即完成了写入到文件的过程。

    注:修改过的脏页面并不会立即更新到文件中,而是有一段时间的延迟,可以调用msync()来强制同步,这样所写的内容就能立即保存到文件里了。

(3)mmap通过/dev/mem映射物理内存
/dev/mem是linux下的一个字符设备,源文件是kernel/drivers/char/mem.c,这个设备文件是专门用来读写物理地址用的。里面的内容是所有物理内存的地址以及内容信息。通常只有root用户对其有读写权限。也就是说,使用mmap时,通过/dev/mem做了一个巧妙的转换,原本填写文件句柄的参数,只需要填上open /dev/mem之后的文件句柄,就可以直接完成对物理内存的映射。

mmap函数入参说明如下:
void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);
参数说明:
start:映射区的开始地址
length:映射区的长度
prot:期望的内存保护标志
—-PROT_EXEC //页内容可以被执行
—-PROT_READ //页内容可以被读取
—-PROT_WRITE //页可以被写入
—-PROT_NONE //页不可访问
flags:指定映射对象的类型
—-MAP_FIXED
—-MAP_SHARED 与其它所有映射这个对象的进程共享映射空间
—-MAP_PRIVATE 建立一个写入时拷贝的私有映射。内存区域的写入不会影响到原文件
—-MAP_ANONYMOUS 匿名映射,映射区不与任何文件关联
fd:如果MAP_ANONYMOUS被设定,为了兼容问题,其值应为-1
offset:被映射对象内容的起点

3、信号量的原理?

前言
  • 将可能被多个执行流同时访问的资源叫做临界资源,临界资源需要进行保护否则会出现数据不一致等问题;
  • 当我们仅用一个互斥锁对临界资源进行保护时,相当于我们将这块临界资源看做一个整体,同一时刻只允许一个执行流对这块临界资源进行访问;
  • 但实际我们可以将这块临界资源再分割为多个区域,当多个执行流需要访问临界资源时,如果这些执行流访问的是临界资源的不同区域,那么我们可以让执行流同时访问临界资源的不同区域,此时不会出现数据不一致等问题。
POSIX信号量

(1) 信号量的概念

信号量本质是一个计数器(不设置全局变量是因为进程间是相互独立的,各进程不一定能看到全局变量,就算看到了也无法保证 ++ 引用计数是原子操作),用于多进程/线程对共享数据对象的读取,它和管道有所不同,它不以传送数据为主要目的,它主要用来保护共享资源(信号量本身也属于临界资源),使得资源在一个时刻只有一个进程/线程独享。

 每个执行流在进入临界区之前都应该先申请信号量,申请成功就有了操作临界区资源的权限,当操作完毕后就应该释放信号量。
在这里插入图片描述
(2)信号量的工作原理
 由于信号量只能进行两种操作,等待和发送信号,即P(sv)和V(sv),它们的行为如下:
 [1] P(sv):我们将申请信号量称为P操作,申请信号量的本质就是申请获得临界资源的使用权限,当申请成功时临界资源中资源的计数器应该减去一,因此P操作的本质就是让计数器减一,如果sv的值大于零,就给它减一;如果它的值为零,就挂起该进程的执行。
 [2] V(sv):我们将释放信号量称为V操作,释放信号量的本质就是归还临界资源中某块资源的使用权限,当释放成功时临界资源中资源的计数器就应该减一,因此V操作的本质就是计数器加一,如果有其他进程因等待sv而被挂起,就让它恢复运行,如果没有进程因等待sv而挂起,就给它加一。

PV操作必须是原子操作,由操作系统保证,通过系统调用实现。

 多个执行流为了访问临界资源会竞争式的申请信号量,因此信号量是会被多个执行流同时访问的,也就是说信号量本质也是临界资源。信号量就是用于保护临界资源的,我们不可能再用信号量去保护信号量,所以信号量的PV操作必须是原子操作。

注意: 内存当中变量的++、–操作并不是原子操作,因此信号量不可能只是简单的对一个全局变量进行++、–操作。

申请信号量失败会被挂起等待。

 当执行流在申请信号量时,可能此时信号量的值为0,也就是说信号量描述的临界资源已经全部被申请了,此时该执行流就应该在该信号量的等待队列中进行等待,直到有信号量被释放时再被唤醒。

注意: 信号量的本质是计数器,但不意味着只有计数器,信号量还包括一个等待队列。

(3)信号量函数
 [1] 初始化信号量
 初始化信号量的函数叫做sem_init,该函数的原型如下:

init sem_init(sem_t *sem, int pshared, unsigned int value);

 参数说明:

  • sem: 需要初始化的信号量
  • pshared: 传入0值表示线程间共享,传入非0值表示进程间共享
  • value: 信号量的初始值(计数器的初始值)

 返回值说明:

  • 初始化信号量成功返回0,失败返回-1

注意: POSIX信号量和System V信号量作用相同,都是用于同步操作,达到无冲突的访问共享资源目的,但POSIX信号量可以用于线程间同步。

 [2] 销毁信号量
 销毁信号量的函数叫做sem_destroy,该函数的原型如下:

int sem_destroy(sem_t *sem);

 参数说明:

  • sem: 需要销毁的信号量

 返回值说明:

  • 销毁信号量成功返回0,失败返回-1

 [3] 申请信号量
 申请信号量的函数叫做sem_wait,该函数的原型如下:

int sem_wait(sem_t *sem);

 参数说明:

  • sem: 需要申请的信号量

 返回值说明:

  • 申请信号量成功返回0,信号量计数器的值减一
  • 申请信号量失败返回-1,信号量计数器的值保持不变

 [4] 释放信号量
 释放信号量的函数叫做sem_post,该函数的原型如下:

int sem_post(sem_t *sem);

 参数说明:

  • sem: 需要释放的信号量

 返回值说明:

  • 释放信号量成功返回0,信号量计数器的值加一
  • 释放信号量失败返回-1,信号量计数器的值保持不变
二元信号量模拟实现互斥功能

二元信号量是最简单的一种锁(互斥锁),它只用两种状态:占用与非占用,所以它的引用计数为1,说明信号量所描述的临界资源只有一份,此时信号量的作用基本就等价于互斥锁。

 例如,下面我们实现一个多线程抢票系统,其中我们用二元信号量模拟实现多线程互斥。
 我们在主线程当中创建四个新线程,让这四个新线程执行抢票逻辑,并且每次抢票完成后打印输出此时剩余的票数,其中我们用全局变量tickets记录当前剩余的票数,此时tickets是会被多个执行流同时访问的临界资源,在下面的代码中我们并没有对tickets进行任何保护操作。

#include <stdio.h>
#include <unistd.h>
#include <pthread.h>

int tickets = 5;
void* TicketGrabbing(void* arg)
{
	while(1)
	{
		if(tickets > 0)
		{
			usleep(1000000);
			printf("Thread-%d get a ticket, %d remaining tickets\n", (int)arg, tickets--);
		}
		else
		{
			break;
		}
	}
	printf("Thread-%d quit...\n", (int)arg);
}
int main()
{
	int ret;
	pthread_t threads[5];
	for(int i = 0; i < 5; i++)
	{
		ret = pthread_create(&threads[i], NULL, TicketGrabbing, (void*)i);
		if(0 != ret)
		{
			printf("Create Thread-%d error...", i);
			return -1;
		}
	}

	for(int j = 0; j < 5; j++)
	{
		pthread_join(threads[j], NULL);
	}
	return 0;
}

编译命令:gcc 1.c -lpthread
pthread 库不是 Linux 系统默认的库,连接时需要使用静态库 libpthread.a,所以在使用pthread_create()创建线程,以及调用 pthread_atfork()函数建立fork处理程序时,需要链接该库

4、自旋锁和信号量有什么区别?

  • 自旋锁
     自旋锁是专为防止多处理器并发而引入的一种锁,它在内核中大量应用于中断处理等部分(对于单处理器来说,为防止中断并发可简单采用关闭中断的方式,不需要自旋锁)。
    自旋锁最多只能被一个内核任务持有,如果一个内核任务试图请求一个已经被持有的自旋锁,那么这个任务就会一直进行忙循环—>旋转—>等待锁重新可用。要是锁未被争用,请求它的内核任务便能立刻得到它并且继续进行。自旋锁可以在任何时刻防止多于一个的内核任务同时进入临界区,因此这种锁可有效地避免多处理器上并发运行的内核任务竞争共享资源。
     事实上,自旋锁的初衷就是:在短时间内进行轻量级的锁定。一个被争用的自旋锁使得请求它的线程在等待锁重新可用的期间进行自旋(特别浪费处理器的时间),所以自旋锁不应该被持有时间过长。如果需要长时间锁定的话,最好使用信号量。
    自旋锁的基本形式如下:
    spin_lock(&mr_lock);
    //临界区
    spin_unlock(&mr_lock);
    
     因为自旋锁在同一时刻只能被最多一个内核任务持有,所以一个时刻只有一个线程允许存在于临界区中。这点很好地满足了对称多处理器需要的锁定服务。在单处理器上,自旋锁仅仅当做一个设置内核抢占的开关。如果内核抢占也不存在,那么自旋锁会在编译时被完全剔除内核。简单地说,自旋锁在内核中主要用来防止多处理器并发访问临界区,防止内核抢占造成的竞争。另外自旋锁不允许任务睡眠(持有自旋锁的任务睡眠会造成死锁----因为睡眠有可能造成持有锁的内核任务被重新调度,而再次申请自己已持有的锁)。它能够在中断上下文中使用

    死锁:假设有两个内核任务和两个资源,每个内核任务都各自持有了一个资源并且等待请求已被对方持有的另一个资源,这便会造成所有的内核任务都互相等待,但它们永远不会释放自己已经占用的资源,于是导致任何内核任务都无法继续运行,这就是发生了死锁。

  • 信号量
     Linux中的信号量是一种睡眠锁。如果有一个任务试图获得一个已被持有的信号量时,信号量会将其推入等待队列,然后让其睡眠。这时处理器获得自由去执行其他代码。当持有信号量的进程把信号量释放后,在等待队列中的一个任务将被唤醒,从而便可以获得这个信号量。信号量的睡眠特性,使得信号量适用于锁会被长时间持有的情况,只能在进程上下文中使用而不能在中断中使用,因为中断上下文是不能被调度的,一旦使用信号量使得中断陷入睡眠便无法唤醒。另外当代码持有信号量时,不可以再持有自旋锁,因为在等待信号量的时候可能会睡眠,而在持有自旋锁时是不允许睡眠的。
    信号量的基本形式如下:
    static DECLARE_MUTEX(mr_sem); //声明互斥信号量
    if(down_interruptible(&mr_sem))
    //可被中断的睡眠,当信号来到,睡眠的任务被唤醒
    //临界区
    up(&mr_sem);
    
    信号量和自旋锁的区别:
    如果代码需要睡眠,使用信号量是唯一的选择。由于不受睡眠的限制,使用信号量通常来说更简单一些。如果需要在自旋锁和信号量中做选择,应该取决于锁被持有的时间长短。理想情况是所有的锁都应该被尽可能短的被持有,但是如果锁的持有时间较长的话,使用信号量是更好的选择。另外信号量不同于自旋锁,它不会关闭内核抢占,所以持有信号量的进程可以随时被内核抢占。这意味着信号量不会对内核调度响应时间带来负面影响。

5、POSIX消息队列如何使用?

概述:
消息队列是Linux IPC中很常见的一种通信方式,它通常用来在不同进程间发送特定格式的消息数据。
消息队列和管道、FIFO有很大的区别,主要有以下两点:

  • 一个进程向消息队列写入消息之前,并不需要某个进程在该队列上等待消息的到达,而管道和FIFO是相反的,进程向其中写消息时,管道和FIFO必须已经打开来读。
  • IPC的持续性不同。当管道和FIFO最后一次关闭发生时,仍在管道和FIFO中的数据会被丢弃。消息队列是随内核的持续性,即一个进程向消息队列写入消息后,然后终止,另外一个进程可以在以后的某个时刻打开该队列读取消息。只要内核没有重启,消息队列就不会被删除。

消息队列可以认为是一个链表。进程(线程)可以往里写消息,也可以从里面取出消息。一个进程可以往某个消息队列里写消息,然后终止,另一个进程随时可以从消息队列里取走这些消息。这里也说明了,消息队列具有随内核的持续性,也就是系统不重启,消息队列永久存在。

注意:
1、消息队列的名字只能以一个’/‘开头,名字中不能包含其他的’/';
2、mq_receive()的第三个参数表示读取消息的长度,不能小于能写入队列中消息的最大值,即一定要大于等于该队列的mq_attr结构中mq_msgsize的大小。
3、消息的优先级:它是一个小于MQ_PRIO_MAX的数,数值越大,优先级越高。POSIX消息队列在调用mq_receive()时总是返回队列中最高优先级的最早消息。如果消息不需要设定优先级,那么可以在mq_send()时置msg_prio为0,mq_receive()msg_prio置为NULL。
4、默认情况下mq_send()mq_receive()是阻塞进行调用,可以通过mq_setattr来设置为O_NONBLOCK,如:

struct mq_attr new_attr;
mq_getattr(mqID, &new_attr);  //获取当前属性
new_attr.mq_flags = O_NONBLOCK;   //设置为非阻塞
mq_setattr(mqID, &new_attr, NULL);   //设置属性

1 POSIX消息队列的创建和关闭
POSIX消息队列的创建,关闭和删除用到以下三个函数接口:

#include <mqueue.h>

mqd_t mq_open(const char *name, int oflag, /*  mode_t mode, struct mq_attr *attr */);   //成功返回消息队列描述符,失败返回-1
mqd_t mq_close(mqd_t mqdes);
mqd_t mq_unlink(const char *name);
//成功返回0,失败返回-1

mq_open()用于打开或创建一个消息队列。
(1)name:表示消息队列的名字,它符合POSIX IPC的名字规则。
(2)oflag:表示打开的方式,和open函数类似。有必须的选项:O_RDONLYO_WRONLYO_RDWR,还有可选的选项:O_NONBLOCKO_CREATO_EXCL
(3)mode:是一个可选参数,在oflag中含有O_CREAT标志且消息队列不存在时,才需要提供该参数,表示默认访问权限,可以参考open。
(4)attr:也是一个可选参数,在oflag中含有O_CREAT标志且消息队列不存在时才需要。该参数用于给新队列设定某些属性,如果是空指针,那么就采用默认属性。
mq_open()返回值是mqd_t类型的值,被称为消息队列的描述符。在Linux2.6.18中该类型的定义为整型:

#include <bits/mqueue.h>
typedef int mqd_t;

mq_close()用于关闭一个消息队列,和文件的close类型,关闭后,消息队列并不从系统中删除。如果一个进程结束,会自动调用关闭打开着的消息队列。
mq_unlink()用于删除一个消息队列。消息队列创建后只有通过调用该函数或者是内核自举才能进行删除。每个消息队列都有一个保存当前打开着描述符数的引用计数器,和文件一样,因此本函数能够实现类似于unlink函数删除一个文件的机制。
创建示例:

#define TEST_MQ_NAME ("/test_mq")

static struct mq_attr test_mq_attr;
static mqd_t test_mq;
test_mq_attr.mq_maxmsg = LOCK_ALARM_MAX_NUM;
test_mq_attr.mq_msgsize = LOCK_ALARM_MAX_SIZE;
mq_unlink(TEST_MQ_NAME);
test_mq = mq_open(TEST_MQ_NAME, O_RDWR|O_CREAT|O_EXCL|O_NONBLOCK, 0644, &lock_mq_attr);

2 POSIX消息队列的属性
POSIX标准规定消息队列属性mq_attr必须要含有以下四个内容:

long mq_flags     //消息队列的标志,0或O_NONBLOCK,用来表示是否阻塞
long mq_maxmsg    //消息队列的最大消息数
long mq_msgsize   //消息队列中每个消息的最大字节数
long mq_curmsgs   //消息队列中当前的消息数目

在Linux2.6.18中mq_attr结构的定义如下:

#include <bits/mqueue.h>

struct mq_attr
{
	long int mq_flags;
	long int mq_maxmsg;
	long int mq_msgsize;
	long int mq_curmsgs;
	long int __pad[4];
};

POSIX消息队列的属性设置和获取可以通过下面两个函数实现:

#include <mqueue.h>

mqd_t mq_getattr(mqd_t mqdes, struct mq_attr *attr);
mqd_t mq_setattr(mqd_t mqdes, struct mq_attr *newattr, struct mq_attr *oldattr);
//成功返回0,失败返回-1

mq_getattr()用于获取当前消息队列的属性,mq_setattr()用于设置当前消息队列的属性。其中mq_setattr()中的oldattr用于保存修改前的消息队列的属性,可以为空。
mq_setattr()可以设置的属性只有mq_flags,用来设置或清除消息队列的非阻塞标志。newattr结构的其他属性被忽略。mq_maxmsgmq_msgsize属性只能在创建消息队列时通过mq_open()来设置。mq_open()只会设置这两个属性,忽略另外两个属性。mq_curmsgs属性只能被获取而不能被设置。
测试代码如下:

#include <iostream>
#include <cstring>
 
#include <errno.h>
#include <unistd.h>
#include <fcntl.h>
#include <mqueue.h>
 
int main()
{
    mqd_t mqID;
    mqID = mq_open("/testmQueue", O_RDWR | O_CREAT, 0666, NULL);
 
    if (mqID < 0)
    {
        printf("open message queue error %s\n", strerror(errno));
        return -1;
    }
 
    mq_attr mqAttr;
    if (mq_getattr(mqID, &mqAttr) < 0)
    {
        printf("get the message queue attribute error...\n");
        return -1;
    }
 
    printf("mq_flags:%d\n", mqAttr.mq_flags);
    printf("mq_maxmsg:%d\n", mqAttr.mq_maxmsg);
    printf("mq_msgsize:%d\n", mqAttr.mq_msgsize);
    printf("mq_curmsgs:%d\n", mqAttr.mq_curmsgs);
}

在Linux2.6.18中执行结果如下:

mq_flags:0
mq_maxmsg:10
mq_msgsize:8192
mq_curmsgs:0

3 POSIX消息队列的使用
POSIX消息队列可以通过以下两个函数来进行发送和接收消息:

#include <mqueue.h>
mqd_t mq_send(mqd_t mqdes, const char *msg_ptr,
                      size_t msg_len, unsigned msg_prio);
                     //成功返回0,出错返回-1
 
mqd_t mq_receive(mqd_t mqdes, char *msg_ptr,
                      size_t msg_len, unsigned *msg_prio);
                     //成功返回接收到消息的字节数,出错返回-1

msg_send()向消息队列中写入一条消息,mq_receive()从消息队列中读取一条消息。
(1)mqdes:消息队列描述符;
(2)msg_prt:指向消息体缓冲区的指针;
(3)msg_len:消息体的长度,其中mq_receive()的该参数不能小于能写入队列中消息的最大大小,即一定要大于等于该队列的mq_attr结构中mq_msgsize的大小。如果mq_receive()中的msg_len小于该值,就会返回EMSGSIZE错误。POXIS消息队列发送的消息长度可以为0;
(4)msg_prio:消息的优先级;它是一个小于MQ_PRIO_MAX的数,数值越大,优先级越高。POSIX消息队列在调用mq_receive()时总是返回队列中最高优先级的最早消息。如果消息不需要设定优先级,那么可以在mq_send()是置msg_prio为0,mq_receive的msg_prio置为NULL。
测试代码如下:

#include <iostream>
#include <cstring>
#include <errno.h>
 
#include <unistd.h>
#include <fcntl.h>
#include <mqueue.h>

int main()
{
    mqd_t mqID;
    mqID = mq_open("/testmQueue", O_RDWR | O_CREAT | O_EXCL, 0666, NULL);
 
    if (mqID < 0)
    {
        if (errno == EEXIST)
        {
            mq_unlink("/testmQueue");
            mqID = mq_open("/testmQueue", O_RDWR | O_CREAT, 0666, NULL);
        }
        else
        {
            printf("open message queue error %s\n", strerror(errno));
            return -1;
        }
    }
 
    if (fork() == 0)
    {
        mq_attr mqAttr;
        mq_getattr(mqID, &mqAttr);
 
        char *buf[mqAttr.mq_msgsize];
 
        for (int i = 1; i <= 5; ++i)
        {
            if (mq_receive(mqID, buf, mqAttr.mq_msgsize, NULL) < 0)
            {
                printf("receive message  failed.\n");
                printf("error info:%s\n", strerror(errno));
                continue;
            }
 
            printf("receive message %d : %s\n", i, buf);   
        }
        return 0;
    }
 
    char msg[] = "yuki";
    for (int i = 1; i <= 5; ++i)
    {
        if (mq_send(mqID, msg, sizeof(msg), i) < 0)
        {
            printf("send message %d failed.\n", i);
            printf("error info:%s\n", strerror(errno));
        }
        printf("send message %d success.\n", i);
        
        sleep(1);
    }
} 

在Linux2.6.18下执行结果如下:

send message 1 success. 
receive message 1: yuki
send message 2 success. 
receive message 2: yuki
send message 3 success. 
receive message 3: yuki
send message 4 success. 
receive message 4: yuki
send message 5 success. 
receive message 5: yuki
  • 28
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值