25、线程同步及消息队列
多个线程共享资源时一定会存在冲突,试想,假设多个线程都要往一段内存中写数据,按照预期应该是一个现程写完数据后,内存地址的偏移增加,另一个线程在紧接着这个偏移地址往下写,每个线程写入的数据都是完整的,但实际上,由于线程是并发的,可能一个线程把自己的数据还没有写完,另外的线程已经开始写了,结果整个内存区就乱成一团了。因此,多线程运行的情况下,线程同步是必需的,也就是说对于共享资源,尤其是“写”资源,当某某个线程正在使用时,必须被独占,其他线程只能等待该线程使用完毕并释放掉之后,才可以使用。同步的基础手段是采用互斥锁和条件变量。
1、线程同步的概念
当多个控制线程共享相同的内存时,需要确保每个线程看到一致的数据视图。如果每个线程使用的变量都是其他线程不会读取或修改的,那么就不会存在一致性问题。同样地,如果变量是只读的,多个线程同时读取该变量也不会有一致性问题。但是,当某个线程可以修改变量,而其他线程也可以读取或修改这个变量的时候,就需要对这些线程进行同步,以确保它们在访问变量的存储内容时不会访问到无效的数值。
当一个线程修改变量时,其他线程在读取这个变量的值时就可能会看到不一致的数据。在变量修改时间多于一个存储器访问周期的处理器结构中,当存储器读与存储器写这两个周期交叉时,这种潜在的不一致性就会出现。当然,这种行为是与处理器结构相关的,但是可移植性程序并不能对使用何种处理器结果作出假设。
图11-2描述了两个线程读写相同变量的假设例子。在这个例子中,线程A读取变量然后给这个变量赋予一个新的值,但写操作需要两个存储器周期。当线程B在这两个存储器写周期中间读取这个相同的变量时,它就会得到不一致的值。
为了解决这个问题,线程不得不使用锁,在同一时间只允许一个线程访问该变量。图11-3描述了这种同步。如果线程B希望读取变量,它首先要获取锁;同样地,当线程A更新变量时,也需要获取这把同样的锁。因而线程B在线程A释放锁以前不能读取变量。
当两个或多个线程试图在同一时间修改同一变量时,也需要进行同步。考虑变量递增操作的情况(图11-4),增量操作通常可分为三步:
(1)从内存单元读入寄存器。
(2)在寄存器中进行变量值的增加。
(3)把新的值写回内存单元。
两个非同步的线程对同一个变量做增量操作
如果两个线程试图在几乎同一时间对同一变量做增量操作而不进行同步的话,结果就可能出现不一致。变量可能比原来增加了1,也有可能比原来增加了2,具体是1还是2取决于第二个线程开始操作时获取的数值。如果第二个线程执行第一步要比第一个线程执行第三步早,第二个线程读到的初始值就与第一个线程一样,它为变量加1,然后再写回去,事实上没有实际的效果,总的来说变量只增加了1。
如果修改操作是原子操作,那么就不存在竞争。在前面的例子中,如果增加1只需要一个存储器周期,那么就没有竞争存在。如果数据总是以顺序一致的方式出现,就不需要额外的同步。当多个线程并不能观察到数据的不一致时,那么操作就是顺序一致的。在现代计算机系统中,存储访问需要多个总线周期,多处理器的总线周期通常在多个处理器上是交叉的,所以无法保证数据是顺序一致的。
在顺序一致的环境中,可以把数据修改操作解释为运行线程的顺序操作步骤。可以把这样的情形描述为“线程A对变量增加了1,然后线程B对变量增加了1,所以变量的值比原来的大2”,或者描述为“线程B对变量增加了1,然后线程A对变量增加了1,所以变量的值比原来的大2”。这两个线程的任何操作顺序都不可能让变量出现除了上述值以外的其他数值。
除了计算机体系结构的因素以外,程序使用变量的方式也会引起竞争,也会导致不一致的情况发生。例如,可能会对某个变量加1,然后基于这个数值作出某种决定。增量操作这一步和作出决定这一步两者的组合并非原子操作,因而给不一致情况的出现提供了可能。
2、互斥锁
可以通过使用pthread的互斥接口保护数据,确保同一时间只有一个线程访问数据。互斥量(mutex)从本质上说是一把锁,在访问共享资源前对互斥量进行加锁,在访问完成后释放互斥量上的锁。对互斥量进行加锁以后,任何其他试图再次对互斥量加锁的线程将会被阻塞直到当前线程释放该互斥锁。如果释放互斥锁时有多个线程阻塞,所有在该互斥锁上的阻塞线程都会变成可运行状态,第一个变为运行状态的线程可以对互斥量加锁,其他线程将会看到互斥锁依然被锁住,只能回去再次等待它重新变为可用。在这种方式下,每次只有一个线程可以向前执行。
在设计时需要规定所有的线程必须遵守相同的数据访问规则,只有这样,互斥机制才能正常工作。操作系统并不会做数据访问的串行化。如果允许其中的的某个线程在没有得到锁的情况下也可以访问共享资源,那么即使其他的线程在使用共享资源前都获取了锁,也还是会出现数据不一致的问题。
互斥变量用pthread_mutex_t数据类型表示,在使用互斥变量以前,必须首先对它进行初始化,可以把它置为常量PTHREAD_MUTEX_INITIALIZER(只对静态分配的互斥量),也可以通过调用pthread_mutex_init函数进行初始化。如果动态地分配互斥量(例如通过调用malloc函数),那么在释放内存前需要调用pthread_mutex_destroy。
#include <pthread.h> int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr); int pthread_mutex_destroy(pthread_mutex_t *mutex); 返回值:若成功则返回0,否则返回错误编号
要用默认的属性初始化互斥量,只需把attr设置为NULL。
对互斥量进行加锁,需要调用pthread_mutex_lock,如果互斥量已经上锁,调用线程将阻塞直到互斥量被解锁。对互斥量解锁,需要调用pthread_mutex_unlock。其函数原型如下:
#include <pthread.h> int pthread_mutex_lock(pthread_mutex_t *mutex); int pthread_mutex_trylock(pthread_mutex_t *mutex); int pthread_mutex_unlock(pthread_mutex_t *mutex); 返回值:若成功则返回0,否则返回错误编号
如果现场不希望被阻塞,他可以使用pthread_mutex_trylock()尝试对互斥量进行加锁。如果调用函数pthread_mutex_trylock()时,互斥量处于未锁住状态,那么函数pthread_mutex_trylock()就会锁住互斥量,不会出现阻塞并返回0;否则pthread_mutex_trylock()就会失败,不能锁住互斥量,而返回EBUSY。
示例:使用互斥量实现线程同步。
pthread.c
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <pthread.h> int num1=0; int num2=0; pthread_mutex_t mutex; void *func(void *arg) { while(1) { num1++; num2++; if(num1!=num2) { printf("%d!=%d\n",num1,num2); } } } int main(int atgc,char *argv[]) { pthread_t tid1,tid2; int err; err=pthread_mutex_init(&mutex,NULL); if(err!=0) { perror("pthread_mutex_init()"); exit(1); } err=pthread_create(&tid1,NULL,func,NULL); if(err!=0) { perror("pthread_creat()"); exit(1); } err=pthread_create(&tid2,NULL,func,NULL); if(err!=0) { perror("pthread_creat()"); exit(1); } while(1) { sleep(3); } exit(0); }
编译运行结果如下,按ctrl+c停止
该示例程序中,num1和num2均初始化为0,按照常理,num1递增和num2递增之后也应相等,但是输出的结果却不相等,造成上面现象的原因很多,例如num1在线程tid1中递增后,立即切换到tid2,在线程tid2中完成num1和num2的递增,从而造成num1与num2的不相等。
修改线程函数如下
void *func(void *arg) { pthread_mutex_lock(&mutex);//加入互斥锁 while(1) { num1++; num2++; if(num1!=num2) { printf("%d!=%d\n",num1,num2); pthread_mutex_unlock(&mutex);//解除互斥锁 } } }
编译运行结果如下:
此时,程序不再有任何输出,原因在于在每个线程中,在num1和num2递增之前加锁互斥量,在num1和num2递增判断之后解锁互斥量,保证了在互斥量加锁期间,num1和num2的变化相同。
3、自旋锁
自旋锁与互斥锁功能一样,唯一一点不同的就是互斥量阻塞后休眠让出cpu,而自旋锁阻塞后不会让出cpu,会一直忙等待,直到得到锁!!!
自旋锁在用户态使用的比较少,在内核使用的比较多!自旋锁的使用场景:锁的持有时间比较短,或者说小于2次上下文切换的时间。
自旋锁在用户态的函数接口和互斥量一样,把pthread_mutex_xxx()中mutex换成spin,如:pthread_spin_init()。
4、读写锁
上面介绍的互斥量,它只有两个状态,要么是加锁状态,要么是不加锁。假如现在一个线程 a 只是想读一个共享变量 i ,因为不确定是否会有线程去写他,所以我们还是要对它进行加锁。但是这时候又一个线程 b 试图读共享变量i ,于是发现被锁住,那么b不得不等到a释放了锁后才能获得锁并读取 i 的值,但是两个读取操作即使是几乎同时发生也并不会像写操作那样造成竞争,因为他们不修改变量的值。所以我们期望如果是 多个线程试图读取共享变量的 值的话,那么他们应该可以立刻获取而不需要等待前一个线程释放因为读而加的锁。读写锁可以很好的解决上面的问题。他提供了比互斥量跟好的并行性。因为以读模式加锁后当又有多个线程仅仅是试图再以读模式加锁然时,并不会造成这些线程阻塞在等待锁的释放上。
读写锁是互斥锁的一种改进,对于读操作远多于写操作的程序,使用读写锁更能提高效率。其实际是一种特殊的自旋锁,它把对共享资源的访问者划分成读者和写者,读者只对共享资源进行读访问,写者则需要对共享资源进行写操作。
读写锁的特点是:
(1)当读写锁是写加锁时,在这个锁被解锁之前,所有试图对这个锁加锁的线程都会被阻塞。
(2)当读写锁是读加锁时,在这个锁被解锁之前,所有试图以读模式对他进行加锁的线程都可以得到访问权,但是如果线程以写模式对此锁加锁时会造成阻塞,直到所有线程释放读锁
读写锁需要使用动态初始化,使用完成后需要销毁,初始化和销毁函数原型如下:
int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr); int pthread_rwlockattr_destory(pthread_rwlockattr_t *attr);
两个函数的用法和互斥锁的初始化销毁相同。
读加锁和写加锁函数原型如下:
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock); int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
非阻塞方式的读加锁和写加锁原型如下:
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock); int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
释放锁函数原型如下:
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
无论是读加锁还是写加锁,释放锁函数都是同一个。以上函数成功后返回0,其他为错误原因。
示例:创建4个线程,2个线程读锁,2个线程写锁,观察4个线程进入临界区的顺序:
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <pthread.h> /* 初始化读写锁 */ pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER; /* 全局资源 */ int global_num = 10; void err_exit(const char *err_msg) { printf("error:%s\n", err_msg); exit(1); } /* 读锁线程函数 */ void *thread_read_lock(void *arg) { char *pthr_name = (char *)arg; while (1) { /* 读加锁 */ pthread_rwlock_rdlock(&rwlock); printf("线程%s进入临界区,global_num = %d\n", pthr_name, global_num); sleep(1); printf("线程%s离开临界区...\n", pthr_name); /* 读解锁 */ pthread_rwlock_unlock(&rwlock); sleep(1); } return NULL; } /* 写锁线程函数 */ void *thread_write_lock(void *arg) { char *pthr_name = (char *)arg; while (1) { /* 写加锁 */ pthread_rwlock_wrlock(&rwlock); /* 写操作 */ global_num++; printf("线程%s进入临界区,global_num = %d\n", pthr_name, global_num); sleep(1); printf("线程%s离开临界区...\n", pthr_name); /* 写解锁 */ pthread_rwlock_unlock(&rwlock); sleep(2); } return NULL; } int main(void) { pthread_t tid_read_1, tid_read_2, tid_write_1, tid_write_2; /* 创建4个线程,2个读,2个写 */ if (pthread_create(&tid_read_1, NULL, thread_read_lock, "read_1") != 0) err_exit("create tid_read_1"); if (pthread_create(&tid_read_2, NULL, thread_read_lock, "read_2") != 0) err_exit("create tid_read_2"); if (pthread_create(&tid_write_1, NULL, thread_write_lock, "write_1") != 0) err_exit("create tid_write_1"); if (pthread_create(&tid_write_2, NULL, thread_write_lock, "write_2") != 0) err_exit("create tid_write_2"); /* 随便等待一个线程,防止main结束 */ if (pthread_join(tid_read_1, NULL) != 0) err_exit("pthread_join()"); return 0; }
编译运行结果如下
可以看到,读锁可以一起进入临界区,而写锁在临界区里面等1秒都不会有其他线程能进来!!!
5、死锁
互斥锁使用不当的时候容易出现死锁,死锁有两种情况,
(1)当一个线程试图对一个互斥锁加锁两次时会出现死锁,一般出现这种情况是因为函数外边进行了一次加锁,而调用的函数内部又尝试去加锁(包括函数的递归调用),从而导致死锁。因此,一般情况下,对于加解锁中间代码段不建议有函数,应该只是一些简单操作。
(2)另一种情况是,当多个线程有多个互斥锁,并且需要相互加锁是容易发生死锁。比如A线程加锁了互斥锁x,又在请求互斥锁y,而B线程加锁了互斥锁y,又在请求互斥锁x,结果就发生了死锁。这种情况需要我们在编程的时候清楚互斥锁的顺序,从而避免出现这种情况。
6、条件变量
互斥锁用于上锁,条件变量用于等待。这两种不同类型的同步都是需要的。条件变量的主要作用是等待某个条件,如果条件不满足,则线程被投入睡眠,当条件满足后,线程将被唤醒继续工作。条件变量总是和互斥锁一块使用。
Linux系统使用pthread_cond_t类型来标识条件变量。
1、条件变量初始化
条件变量的初始化和互斥锁类似,可以使用静态初始化,也可以动态分配;如果使用静态初始化,可以做如下定义:
pthread_cond_t cond=PTHREAD_COND_INTITIALIZER;
如果需要使用动态分配的,则使用以下来给你个函数进行:
int pthread_cond_init(pthread_cond_t *cv,const pthread_condattr_t *cattr); int pthread_cond_destory(pthread_cond_t *cv);
以上两个函数成功则返回0,其他为错误。同样如果只是用条件变量的默认属性的话,初始化函数的第二个参数置位NULL即可。以上两个函数成功则返回0,其他为错误原因。
2、条件变量等待
条件变量的作用就是等待某个条件。等待函数有两个,一个是定时等待,一个是无限等待。这两个函数原型如下:
int pthread_cond_wait(pthread_cond_t *cv,pthread_mutex_t *mutex); int pthread_cond_timedwait(pthread_cond_t *cv,pthread_mutex_t *mp,const struct timespec *abstime);
以上两个函数成功则返回,其他为错误原因。
一个线程调用函数pthread_cond_wait()在一个条件变量上发生阻塞等待,需要进行以下三个步骤:
(1)释放互斥量。
(2)阻塞等待。
(3)当被唤醒时,重新获得互斥量并返回。
需要注意到的是,等待时间abstime是一个绝对是件,指得是1970年1月1日0时起过去的秒数和纳秒数。通常的用法,比如说等待3分钟,先获取当前时间的总秒数,然后再加上3分钟的秒数。即180秒。abtime的定义如下:
struct timespec { time_t tv_sec; long tv_nsec; }
当等待时间到达时,条件还没有满足,即pthread_cond_timedwait函数返回ETIMEDOUT错误。
3、条件变量通知
当某个线程在等待条件时进入睡眠,就需要条件满足后通知唤醒睡眠的线程去执行。通知函数有两个:一个是通知单个的线程,一个是广播通知。一般情况下,除非特别明确哪一个线程在等待条件变量时使用单线程通知函数,否则全部使用广播通知函数。以下为两个函数原型:
int pthread_cond_signal(pthread_cond_t *cv); int pthread_cond_broadcast(pthread_cond_t *cv);
以上两个函数成功返回0,其他为错误原因。
示例:
使用条件变量实现线程的同步。
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <pthread.h> struct msg { struct msg *next; int num; }; struct msg *head; pthread_cond_t has_product=PTHREAD_COND_INITIALIZER; pthread_mutex_t lock=PTHREAD_MUTEX_INITIALIZER; void *consumer(void *p) { struct msg *mp; while(1) { pthread_mutex_lock(&lock); while(head==NULL) pthread_cond_wait(&has_product,&lock); mp=head; head=mp->next; pthread_mutex_unlock(&lock); printf("Consume%d\n",mp->num); free(mp); sleep(rand()%5); } } void *producer(void *p) { struct msg *mp; while(1) { mp=malloc(sizeof(struct msg)); mp->num=rand()%1000+1; printf("Prpduce%d\n",mp->num); pthread_mutex_lock(&lock); mp->next=head; head=mp; pthread_mutex_unlock(&lock); pthread_cond_signal(&has_product); sleep(rand()%5); } } int main(int argc,char *argv[]) { pthread_t tid1,tid2; srand(time(NULL));//随机数发生器 pthread_create(&tid1,NULL,producer,NULL); pthread_create(&tid2,NULL,consumer,NULL); pthread_join(tid1,NULL); pthread_join(tid2,NULL); exit(1); }
编译运行结果如下
7、信号量参考进程通信。
8、消息队列
POSIX消息队列是给定进程中各线程的一种通信方式.POSIX指得是可移植性操作系统接口(Portable Operating System Interface),其系列函数如下:
1、mq_open
mq_open函数用来创建或者打开一个消息队列,该函数的原型如下:
mqd_t mq_open(const char* name,int oflag,.../*mode_t mode,struct mq_attr* attr */
该函数处理两个必需的参数外还有两个变参,各参数含义如下。
name:用于创建或打开消息队列的名字。POSIX要求该名字必须以”/“开头,并且后边不允许再出现”/“符号,比如”/MsgQueue".在早期UNIX中,如果遵守这样的规定,系统可能会在根目录下去建立消息队列的文件,在没有root权限的情况下,函数的调用将会失败。但现在众多的UNIX\Linux都支持POSIX标准了,也就是说,系统并不会在根目录下建立消息队列文件,可能会在/tmp等目录下建立,也有可能根本就不在磁盘上差个创建文件。
o_flag:消息队列打开模式,与open类似。
后面的两个参数则是需要新创建消息队列时必须指定的两个参数:一个是权限,和文件的权限类似,一个是消息队列属性。
2、mq_close
mq_close函数是关闭一个消息队列。值得注意的是,如同关闭一个文件一样,仅仅是关闭了描述符,之前曾经打开过的文件描述符不再可用,但是物理文件还是存在的,关闭消息队列也是一样的,仅仅是关闭描述符。但实际的消息队列还是存在的,如果要彻底删除则需要使用mq_unlink。mq_close原型如下:
int mq_close(mqd_t mqdes); 该函数成功返回0,失败返回-1.
3、mq_unlink
mq_unlink函数是从物理上删除一个消息队列。函数原型如下:
int mq_unlink(const char* name);
该函数成功返回0,失败返回-1.
4、mq_getarrt和mq_setarrt
这两个函数分别是用来获取消息队列的属性和设置消息队列的属性,函数原型如下:
int mq_getattr(mqd_t mqdes,struct mq_attr* attr); int mq_setattr(mqd_t mqdes,const struct mq_attr* attr, struct mq_attr* oattr);
函数成功返回0,失败返回-1.
在mq_open函数中已经介绍了消息队列的属性。对于mq_setattr函数来讲,这些属性中唯一可用的就是设置消息队列为非阻塞模式。该函数中第二个参数是需要设置的属性,第三个参数是返回原来的属性。
5、mq_send和mq_receive
mq_send和mq_receive分别是发送消息和接收消息函数,两个函数的原型如下:
int mq_send(mqd_t mqdes,char* ptr,size_t len,unsigned int prio); ssize_t mq_receive(mqd_t mqdes,char* ptr ,size_t len,unsigned int* prio);
函数成功返回0,失败返回-1.
这两个函数的参数基本差不多。第一个参数是消息队列描述符,第二个参数是发送(接收)消息的缓冲区,第三个参数是发送(接收)消息的长度,第四个参数是优先级。
在mq_receive 函数中,参数len的长度必须大于等于该消息队列的单个消息最大长度,即消息队列属性中msgsize,否则将会立即返回EMSGSIZE错误。优先级制定为0,即发送函数不使用优先级。接收函数中,如果优先级参数非空,则将当前接收到的消息优先级填入。
6、mq_notify
mq_notify就是为消息队列产生一个异步的事件通知。当程序调用mq_receive函数从一个消息队列接收消息时,有两种情况:一种是消息队列为阻塞模式;一种是非阻塞模式。当消息队列处于阻塞模式的时候,调用mq_receive函数接收消息。如果没有消息的话,则会阻塞在mq_receive调用。当消息队列处于非阻塞模式的时候,如果没有消息,则会立即返回。如果一个单进程、单线程的应用,只从一个消息队列接受消息,那么使用阻塞方式,程序工作就会很正常。但是,如果需要从多个消息队列中接收消息,显然阻塞方式下根本没有办法处理。而非阻塞方式可以处理多个消息队列,但需要使用轮询的方式去探测,很显然这是对于CPU一种极大的浪费。于是在这种情况下,我们使用一种异步的通知机制来完成从消息队列接收消息的处理,而mq_notify函数的作用就是发出通知。mq_notify函数原型如下:
int mq_notify(mqd_t mqdes,const sruct sigevent* notification(; 成功返回0,否则返回-1
参数mqdes:消息队列描述符。
参数notification:产生通知的相关行为。
struct sigevent定义如下:
union signal { int sival_int; void* sival_ptr; } struct sigevaent { int sigev_notify; int sigval sigev_value; void(*sigev_notify_function)(union sigval); pthread_attr_t* sigev_notify_attributes; }
sigev_notify表示通知的处理方式,取值为SIGEV_NONE,SIGEV_SIGANAL,SIGEV_SIGNAL,SIGEV_THREAD.SIGEV_NONE表示什么都不做,SIGEV_SIGNAL表示产生一个信号,SIGEV_THREAD表示启动一个线程处理。
sigev_signo表示要产生的信号值(信号编号,比如SIGUSR1).
sigev_value表示需要传递个新启动的线程的参数。
sigev_notify_function表示线程入口函数。
sigev_notify_attributes表示新启动的线程的属性。
需要注意的是,部分操作系统可能并不支持取值为SIGEV_THREAD的形式。当取值为SIGEV_SIGNAL时,后边的三个字段都不需要填写,只需要填写sigev_signo即可。
在使用mq_notify函数时,如果参数notification为空指针,并且当前进程已经注册到该消息队列,再如此调用的话,将会取消这个注册。如果notification为非空,则表示当前进程注册到该消息队列,并希望收到消息到达的通知。
另外需要特别注意的有以下几点:
(1)任意时刻只有一个进程可以被注册为接收某个消息队列的通知。也就是说,不可能有2个或以上的进程同时注册在某个消息队列上等候通知。
(2)如果通知被发送到该注册的进程后,改注册立刻被取消。也就是说,如果还需要通知的话,则需要再次调用mq_notify 函数进行注册。
(3)只有消息放置到空的消息队列的时候,才会发出通知。也就是说,如果接收进程处理一个消息比较慢,导致消息队列已经有积攒的消息,这个时候,新的消息再加入到消息队列的时候,是不会产生通知的,除非阿精消息队列中的所有消息全部收空。
(4)使用mq_reiveive以阻塞的方式调用的优先级方式比通知的优先级高。也就是说,假设有个进程以阻塞方式调用mq_receive接收消息队列消息,另外一个进程使用注册方式进行接收同一个消息队列的消息,那么注册方式的进程永远都不会收到通知。
7、消息队列的限制
在创建消息队列的时候有两个字段mq_maxmsg和mq_msgsize,分别是最大消息个数和单个消息最大长度,很显然这两个字段及决定了需要占用的最大虚拟内存或者磁盘空间。POSIX并没有对这两个值进行限制,因此在实际使用中,应该根据使用的操作系统进行设定。
另外,在消息队列使用上,有如下其他两个限制。
MQ_OPEN_MAX:一个进程能偶同时打开的消息队列最大个数,POSIX要求至少为8.
MQ_PRIO_MAX:任意消息的最大优先级值加1,POSIX要求至少为32.