linux等待队列?redis消息队列
linux 多进程信号同步问题
朋友你好:希望能帮到你。互相学习。
线程的最大特点是资源的共享性,但资源共享中的同步问题是多线程编程的难点。linux下提供了多种方式来处理线程同步,最常用的是互斥锁、条件变量和信号量。
1)互斥锁(mutex)
通过锁机制实现线程间的同步。同一时刻只允许一个线程执行一个关键部分的代码。
int pthread_mutex_init(pthread_mutex_t*mutex,const pthread_mutex_attr_t*mutexattr);
int pthread_mutex_lock(pthread_mutex*mutex);
int pthread_mutex_destroy(pthread_mutex*mutex);
int pthread_mutex_unlock(pthread_mutex*
(1)先初始化锁init()或静态赋值pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIER
attr_t有:
PTHREAD_MUTEX_TIMED_NP:其余线程等待队列
PTHREAD_MUTEX_RECURSIVE_NP:嵌套锁,允许线程多次加锁,不同线程,解锁后重新竞争
PTHREAD_MUTEX_ERRORCHECK_NP:检错,与一同,线程请求已用锁,返回EDEADLK;
PTHREAD_MUTEX_ADAPTIVE_NP:适应锁,解锁后重新竞争
(2)加锁,lock,trylock,lock阻塞等待锁,trylock立即返回EBUSY
(3)解锁,unlock需满足是加锁状态,且由加锁线程解锁
(4)清除锁,destroy(此时锁必需unlock,否则返回EBUSY,//Linux下互斥锁不占用内存资源
示例代码
#include<cstdio>
#include<cstdlib>
#include<unistd.h>
#include<pthread.h>
#include"iostream"
using namespace std;
pthread_mutex_t mutex= PTHREAD_MUTEX_INITIALIZER;
int tmp;
void* thread(void*arg)
{
cout<<"thread id is"<< pthread_self()<< endl;
pthread_mutex_lock(&mutex);
tmp= 12;
cout<<"Now a is"<< tmp<< endl;
pthread_mutex_unlock(&mutex);
return NULL;
}
int main()
{
pthread_t id;
cout<<"main thread id is"<< pthread_self()<< endl;
tmp= 3;
cout<<"In main func tmp="<< tmp<< endl;
if(!pthread_create(&id, NULL, thread, NULL))
{
cout<<"Create thread success!"<< endl;
}
else
{
cout<<"Create thread failed!"<< endl;
}
pthread_join(id, NULL);
pthread_mutex_destroy(&mutex);
return 0;
}
编译: g++-o thread testthread.cpp-lpthread
说明:pthread库不是Linux系统默认的库,连接时需要使用静态库libpthread.a,所以在使用pthread_create()创建线程,以及调用pthread_atfork()函数建立fork处理程序时,需要链接该库。在编译中要加-lpthread参数。
2)条件变量(cond)
利用线程间共享的全局变量进行同步的一种机制。条件变量上的基本操作有:触发条件(当条件变为 true时);等待条件,挂起线程直到其他线程触发条件。
int pthread_cond_init(pthread_cond_t*cond,pthread_condattr_t*cond_attr);
int pthread_cond_wait(pthread_cond_t*cond,pthread_mutex_t*mutex);
int pthread_cond_timewait(pthread_cond_t*cond,pthread_mutex*mutex,const timespec*abstime);
int pthread_cond_destroy(pthread_cond_t*cond);
int pthread_cond_signal(pthread_cond_t*cond);
int pthread_cond_broadcast(pthread_cond_t*cond);//解除所有线程的阻塞
(1)初始化.init()或者pthread_cond_t cond=PTHREAD_COND_INITIALIER(前者为动态初始化,后者为静态初始化);属性置为NULL
(2)等待条件成立.pthread_wait,pthread_timewait.wait()释放锁,并阻塞等待条件变量为真,timewait()设置等待时间,仍未signal,返回ETIMEOUT(加锁保证只有一个线程wait)
(3)激活条件变量:pthread_cond_signal,pthread_cond_broadcast(激活所有等待线程)
(4)清除条件变量:destroy;无线程等待,否则返回EBUSY
对于
int pthread_cond_wait(pthread_cond_t*cond, pthread_mutex_t*mutex);
int pthread_cond_timedwait(pthread_cond_t*cond, pthread_mutex_t*mutex, const struct timespec*abstime);
一定要在mutex的锁定区域内使用。
如果要正确的使用pthread_mutex_lock与pthread_mutex_unlock,请参考
pthread_cleanup_push和pthread_cleanup_pop宏,它能够在线程被cancel的时候正确的释放mutex!
另外,posix1标准说,pthread_cond_signal与pthread_cond_broadcast无需考虑调用线程是否是mutex的拥有者,也就是说,可以在lock与unlock以外的区域调用。如果我们对调用行为不关心,那么请在lock区域之外调用吧。
说明:
(1)pthread_cond_wait自动解锁互斥量(如同执行了pthread_unlock_mutex),并等待条件变量触发。这时线程挂起,不占用CPU时间,直到条件变量被触发(变量为ture)。在调用 pthread_cond_wait之前,应用程序必须加锁互斥量。pthread_cond_wait函数返回前,自动重新对互斥量加锁(如同执行了pthread_lock_mutex)。
(2)互斥量的解锁和在条件变量上挂起都是自动进行的。因此,在条件变量被触发前,如果所有的线程都要对互斥量加锁,这种机制可保证在线程加锁互斥量和进入等待条件变量期间,条件变量不被触发。条件变量要和互斥量相联结,以避免出现条件竞争——个线程预备等待一个条件变量,当它在真正进入等待之前,另一个线程恰好触发了该条件(条件满足信号有可能在测试条件和调用pthread_cond_wait函数(block)之间被发出,从而造成无限制的等待)。
(3)pthread_cond_timedwait和 pthread_cond_wait一样,自动解锁互斥量及等待条件变量,但它还限定了等待时间。如果在abstime指定的时间内cond未触发,互斥量mutex被重新加锁,且pthread_cond_timedwait返回错误 ETIMEDOUT。abstime参数指定一个绝对时间,时间原点与 time和 gettimeofday相同:abstime= 0表示 1970年1月1日00:00:00 GMT。
(4)pthread_cond_destroy销毁一个条件变量,释放它拥有的资源。进入 pthread_cond_destroy之前,必须没有在该条件变量上等待的线程。
(5)条件变量函数不是异步信号安全的,不应当在信号处理程序中进行调用。特别要注意,如果在信号处理程序中调用 pthread_cond_signal或pthread_cond_boardcast函数,可能导致调用线程死锁。
示例程序1
#include<stdio.h>
#include<pthread.h>
#include"stdlib.h"
#include"unistd.h"
pthread_mutex_t mutex;
pthread_cond_t cond;
void hander(void*arg)
{
free(arg);
(void)pthread_mutex_unlock(&mutex);
}
void*thread1(void*arg)
{
pthread_cleanup_push(hander,&mutex);
while(1)
{
printf("thread1 is running\n");
pthread_mutex_lock(&mutex);
pthread_cond_wait(&cond,&mutex);
printf("thread1 applied the condition\n");
pthread_mutex_unlock(&mutex);
sleep(4);
}
pthread_cleanup_pop(0);
}
void*thread2(void*arg)
{
while(1)
{
printf("thread2 is running\n");
pthread_mutex_lock(&mutex);
pthread_cond_wait(&cond,&mutex);
printf("thread2 applied the condition\n");
pthread_mutex_unlock(&mutex);
sleep(1);
}
}
int main()
{
pthread_t thid1,thid2;
printf("condition variable study!\n");
pthread_mutex_init(&mutex,NULL);
pthread_cond_init(&cond,NULL);
pthread_create(&thid1,NULL,thread1,NULL);
pthread_create(&thid2,NULL,thread2,NULL);
sleep(1);
do
{
pthread_cond_signal(&cond);
}while(1);
sleep(20);
pthread_exit(0);
return 0;
}
示例程序2:
#include<pthread.h>
#include<unistd.h>
#include"stdio.h"
#include"stdlib.h"
static pthread_mutex_t mtx= PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t cond= PTHREAD_COND_INITIALIZER;
struct node
{
int n_number;
struct node*n_next;
}*head= NULL;
/*[thread_func]*/
static void cleanup_handler(void*arg)
{
printf("Cleanup handler of second thread./n");
free(arg);
(void)pthread_mutex_unlock(&mtx);
}
static void*thread_func(void*arg)
{
struct node*p= NULL;
pthread_cleanup_push(cleanup_handler, p);
while(1)
{
//这个mutex主要是用来保证pthread_cond_wait的并发性
pthread_mutex_lock(&mtx);
while(head== NULL)
{
//这个while要特别说明一下,单个pthread_cond_wait功能很完善,为何
//这里要有一个while(head== NULL)呢?因为pthread_cond_wait里的线
//程可能会被意外唤醒,如果这个时候head!= NULL,则不是我们想要的情况。
//这个时候,应该让线程继续进入pthread_cond_wait
// pthread_cond_wait会先解除之前的pthread_mutex_lock锁定的mtx,
//然后阻塞在等待对列里休眠,直到再次被唤醒(大多数情况下是等待的条件成立
//而被唤醒,唤醒后,该进程会先锁定先pthread_mutex_lock(&mtx);,再读取资源
//用这个流程是比较清楚的/*block-->unlock-->wait() return-->lock*/
pthread_cond_wait(&cond,&mtx);
p= head;
head= head->n_next;
printf("Got%d from front of queue/n", p->n_number);
free(p);
}
pthread_mutex_unlock(&mtx);//临界区数据操作完毕,释放互斥锁
}
pthread_cleanup_pop(0);
return 0;
}
int main(void)
{
pthread_t tid;
int i;
struct node*p;
//子线程会一直等待资源,类似生产者和消费者,但是这里的消费者可以是多个消费者,而
//不仅仅支持普通的单个消费者,这个模型虽然简单,但是很强大
pthread_create(&tid, NULL, thread_func, NULL);
sleep(1);
for(i= 0; i< 10; i++)
{
p=(struct node*)malloc(sizeof(struct node));
p->n_number= i;
pthread_mutex_lock(&mtx);//需要操作head这个临界资源,先加锁,
p->n_next= head;
head= p;
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mtx);//解锁
sleep(1);
}
printf("thread 1 wanna end the line.So cancel thread 2./n");
//关于pthread_cancel,有一点额外的说明,它是从外部终止子线程,子线程会在最近的取消点,退出
//线程,而在我们的代码里,最近的取消点肯定就是pthread_cond_wait()了。
pthread_cancel(tid);
pthread_join(tid, NULL);
printf("All done-- exiting/n");
return 0;
}
3)信号量
如同进程一样,线程也可以通过信号量来实现通信,虽然是轻量级的。
信号量函数的名字都以"sem_"打头。线程使用的基本信号量函数有四个。
#include<semaphore.h>
int sem_init(sem_t*sem, int pshared, unsigned int value);
这是对由sem指定的信号量进行初始化,设置好它的共享选项(linux只支持为0,即表示它是当前进程的局部信号量),然后给它一个初始值VALUE。
两个原子操作函数:
int sem_wait(sem_t*sem);
int sem_post(sem_t*sem);
这两个函数都要用一个由sem_init调用初始化的信号量对象的指针做参数。
sem_post:给信号量的值加1;
sem_wait:给信号量减1;对一个值为0的信号量调用sem_wait,这个函数将会等待直到有其它线程使它不再是0为止。
int sem_destroy(sem_t*sem);
这个函数的作用是再我们用完信号量后都它进行清理。归还自己占有的一切资源。
示例代码:
#include<stdlib.h>
#include<stdio.h>
#include<unistd.h>
#include<pthread.h>
#include<semaphore.h>
#include<errno.h>
#define return_if_fail(p) if((p)== 0){printf("[%s]:func error!/n", __func__);return;}
typedef struct _PrivInfo
{
sem_t s1;
sem_t s2;
time_t end_time;
}PrivInfo;
static void info_init(PrivInfo* thiz);
static void info_destroy(PrivInfo* thiz);
static void* pthread_func_1(PrivInfo* thiz);
static void* pthread_func_2(PrivInfo* thiz);
int main(int argc, char** argv)
{
pthread_t pt_1= 0;
pthread_t pt_2= 0;
int ret= 0;
PrivInfo* thiz= NULL;
thiz=(PrivInfo*)malloc(sizeof(PrivInfo));
if(thiz== NULL)
{
printf("[%s]: Failed to malloc priv./n");
return-1;
}
info_init(thiz);
ret= pthread_create(&pt_1, NULL,(void*)pthread_func_1, thiz);
if(ret!= 0)
{
perror("pthread_1_create:");
}
ret= pthread_create(&pt_2, NULL,(void*)pthread_func_2, thiz);
if(ret!= 0)
{
perror("pthread_2_create:");
}
pthread_join(pt_1, NULL);
pthread_join(pt_2, NULL);
info_destroy(thiz);
return 0;
}
static void info_init(PrivInfo* thiz)
{
return_if_fail(thiz!= NULL);
thiz->end_time= time(NULL)+ 10;
sem_init(&thiz->s1, 0, 1);
sem_init(&thiz->s2, 0, 0);
return;
}
static void info_destroy(PrivInfo* thiz)
{
return_if_fail(thiz!= NULL);
sem_destroy(&thiz->s1);
sem_destroy(&thiz->s2);
free(thiz);
thiz= NULL;
return;
}
static void* pthread_func_1(PrivInfo* thiz)
{
return_if_fail(thiz!= NULL);
while(time(NULL)< thiz->end_time)
{
sem_wait(&thiz->s2);
printf("pthread1: pthread1 get the lock./n");
sem_post(&thiz->s1);
printf("pthread1: pthread1 unlock/n");
sleep(1);
}
return;
}
static void* pthread_func_2(PrivInfo* thiz)
{
return_if_fail(thiz!= NULL);
while(time(NULL)< thiz->end_time)
{
sem_wait(&thiz->s1);
printf("pthread2: pthread2 get the unlock./n");
sem_post(&thiz->s2);
printf("pthread2: pthread2 unlock./n");
sleep(1);
}
return;
}
通过执行结果后,可以看出,会先执行线程二的函数,然后再执行线程一的函数。它们两就实现了同步
Linux系统中的进程调度介绍
操作系统要实现多进程,进程调度必不可少。
有人说,进程调度是操作系统中最为重要的一个部分。我觉得这种说法说得太绝对了一点,就像很多人动辄就说"某某函数比某某函数效率高XX倍"一样,脱离了实际环境,这些结论是比较片面的。
而进程调度究竟有多重要呢?首先,我们需要明确一点:进程调度是对TASK_RUNNING状态的进程进行调度(参见《linux进程状态浅析》)。如果进程不可执行(正在睡眠或其他),那么它跟进程调度没多大关系。
所以,如果你的系统负载非常低,盼星星盼月亮才出现一个可执行状态的进程。那么进程调度也就不会太重要。哪个进程可执行,就让它执行去,没有什么需要多考虑的。
反之,如果系统负载非常高,时时刻刻都有N多个进程处于可执行状态,等待被调度运行。那么进程调度程序为了协调这N个进程的执行,必定得做很多工作。协调得不好,系统的性能就会大打折扣。这个时候,进程调度就是非常重要的。
尽管我们平常接触的很多计算机(如桌面系统、网络服务器、等)负载都比较低,但是linux作为一个通用操作系统,不能假设系统负载低,必须为应付高负载下的进程调度做精心的设计。
当然,这些设计对于低负载(且没有什么实时性要求)的环境,没多大用。极端情况下,如果CPU的负载始终保持0或1(永远都只有一个进程或没有进程需要在CPU上运行),那么这些设计基本上都是徒劳的。
优先级
现在的操作系统为了协调多个进程的“同时”运行,最基本的手段就是给进程定义优先级。定义了进程的优先级,如果有多个进程同时处于可执行状态,那么谁优先级高谁就去执行,没有什么好纠结的了。
那么,进程的优先级该如何确定呢?有两种方式:由用户程序指定、由内核的调度程序动态调整。(下面会说到)
linux内核将进程分成两个级别:普通进程和实时进程。实时进程的优先级都高于普通进程,除此之外,它们的调度策略也有所不同。
实时进程的调度
实时,原本的涵义是“给定的操作一定要在确定的时间内完成”。重点并不在于操作一定要处理得多快,而是时间要可控(在最坏情况下也不能突破给定的时间)。
这样的“实时”称为“硬实时”,多用于很精密的系统之中(比如什么火箭、导弹之类的)。一般来说,硬实时的系统是相对比较专用的。
像linux这样的通用操作系统显然没法满足这样的要求,中断处理、虚拟内存、等机制的存在给处理时间带来了很大的不确定性。硬件的cache、磁盘寻道、总线争用、也会带来不确定性。
比如考虑“i++;”这么一句C代码。绝大多数情况下,它执行得很快。但是极端情况下还是有这样的可能:
1、i的内存空间未分配,CPU触发缺页异常。而linux在缺页异常的处理代码中试图分配内存时,又可能由于系统内存紧缺而分配失败,导致进程进入睡眠;
2、代码执行过程中硬件产生中断,linux进入中断处理程序而搁置当前进程。而中断处理程序的处理过程中又可能发生新的硬件中断,中断永远嵌套不止……;
等等……
而像linux这样号称实现了“实时”的通用操作系统,其实只是实现了“软实时”,即尽可能地满足进程的实时需求。
如果一个进程有实时需求(它是一个实时进程),则只要它是可执行状态的,内核就一直让它执行,以尽可能地满足它对CPU的需要,直到它完成所需要做的事情,然后睡眠或退出(变为非可执行状态)。
而如果有多个实时进程都处于可执行状态,则内核会先满足优先级最高的实时进程对CPU的需要,直到它变为非可执行状态。
于是,只要高优先级的实时进程一直处于可执行状态,低优先级的实时进程就一直不能得到CPU;只要一直有实时进程处于可执行状态,普通进程就一直不能得到CPU。
那么,如果多个相同优先级的实时进程都处于可执行状态呢?这时就有两种调度策略可供选择:
1、SCHED_FIFO:先进先出。直到先被执行的进程变为非可执行状态,后来的进程才被调度执行。在这种策略下,先来的进程可以执行sched_yield系统调用,自愿放弃CPU,以让权给后来的进程;
2、SCHED_RR:轮转调度。内核为实时进程分配时间片,在时间片用完时,让下一个进程使用CPU;
强调一下,这两种调度策略以及sched_yield系统调用都仅仅针对于相同优先级的多个实时进程同时处于可执行状态的情况。
在linux下,用户程序可以通过sched_setscheduler系统调用来设置进程的调度策略以及相关调度参数;sched_setparam系统调用则只用于设置调度参数。这两个系统调用要求用户进程具有设置进程优先级的能力(CAP_SYS_NICE,一般来说需要root权限)(参阅capability相关的文章)。
通过将进程的策略设为SCHED_FIFO或SCHED_RR,使得进程变为实时进程。而进程的优先级则是通过以上两个系统调用在设置调度参数时指定的。
对于实时进程,内核不会试图调整其优先级。因为进程实时与否?有多实时?这些问题都是跟用户程序的应用场景相关,只有用户能够回答,内核不能臆断。
综上所述,实时进程的调度是非常简单的。进程的优先级和调度策略都由用户定死了,内核只需要总是选择优先级最高的实时进程来调度执行即可。唯一稍微麻烦一点的只是在选择具有相同优先级的实时进程时,要考虑两种调度策略。
普通进程的调度
实时进程调度的中心思想是,让处于可执行状态的最高优先级的实时进程尽可能地占有CPU,因为它有实时需求;而普通进程则被认为是没有实时需求的进程,于是调度程序力图让各个处于可执行状态的普通进程和平共处地分享CPU,从而让用户觉得这些进程是同时运行的。
与实时进程相比,普通进程的调度要复杂得多。内核需要考虑两件麻烦事:
一、动态调整进程的优先级
按进程的行为特征,可以将进程分为“交互式进程”和“批处理进程”:
交互式进程(如桌面程序、服务器、等)主要的任务是与外界交互。这样的进程应该具有较高的优先级,它们总是睡眠等待外界的输入。而在输入到来,内核将其唤醒时,它们又应该很快被调度执行,以做出响应。比如一个桌面程序,如果鼠标点击后半秒种还没反应,用户就会感觉系统“卡”了;
批处理进程(如编译程序)主要的任务是做持续的运算,因而它们会持续处于可执行状态。这样的进程一般不需要高优先级,比如编译程序多运行了几秒种,用户多半不会太在意;
如果用户能够明确知道进程应该有怎样的优先级,可以通过nice、setpriority系统调用来对优先级进行设置。(如果要提高进程的优先级,要求用户进程具有CAP_SYS_NICE能力。)
然而应用程序未必就像桌面程序、编译程序这样典型。程序的行为可能五花八门,可能一会儿像交互式进程,一会儿又像批处理进程。以致于用户难以给它设置一个合适的优先级。
再者,即使用户明确知道一个进程是交互式还是批处理,也多半碍于权限或因为偷懒而不去设置进程的优先级。(你又是否为某个程序设置过优先级呢?)
于是,最终,区分交互式进程和批处理进程的重任就落到了内核的调度程序上。
调度程序关注进程近一段时间内的表现(主要是检查其睡眠时间和运行时间),根据一些经验性的公式,判断它现在是交互式的还是批处理的?程度如何?最后决定给它的优先级做一定的调整。
进程的优先级被动态调整后,就出现了两个优先级:
1、用户程序设置的优先级(如果未设置,则使用默认值),称为静态优先级。这是进程优先级的基准,在进程执行的过程中往往是不改变的;
2、优先级动态调整后,实际生效的优先级。这个值是可能时时刻刻都在变化的;
二、调度的公平性
在支持多进程的系统中,理想情况下,各个进程应该是根据其优先级公平地占有CPU。而不会出现“谁运气好谁占得多”这样的不可控的情况。
linux实现公平调度基本上是两种思路:
1、给处于可执行状态的进程分配时间片(按照优先级),用完时间片的进程被放到“过期队列”中。等可执行状态的进程都过期了,再重新分配时间片;
2、动态调整进程的优先级。随着进程在CPU上运行,其优先级被不断调低,以便其他优先级较低的进程得到运行机会;
后一种方式有更小的调度粒度,并且将“公平性”与“动态调整优先级”两件事情合而为一,大大简化了内核调度程序的代码。因此,这种方式也成为内核调度程序的新宠。
强调一下,以上两点都是仅针对普通进程的。而对于实时进程,内核既不能自作多情地去动态调整优先级,也没有什么公平性可言。
普通进程具体的调度算法非常复杂,并且随linux内核版本的演变也在不断更替(不仅仅是简单的调整),所以本文就不继续深入了。
调度程序的效率
“优先级”明确了哪个进程应该被调度执行,而调度程序还必须要关心效率问题。调度程序跟内核中的很多过程一样会频繁被执行,如果效率不济就会浪费很多CPU时间,导致系统性能下降。
在linux 2.4时,可执行状态的进程被挂在一个链表中。每次调度,调度程序需要扫描整个链表,以找出最优的那个进程来运行。复杂度为O(n);
在linux 2.6早期,可执行状态的进程被挂在N(N=140)个链表中,每一个链表代表一个优先级,系统中支持多少个优先级就有多少个链表。每次调度,调度程序只需要从第一个不为空的链表中取出位于链表头的进程即可。这样就大大提高了调度程序的效率,复杂度为O(1);
在linux 2.6近期的版本中,可执行状态的进程按照优先级顺序被挂在一个红黑树(可以想象成平衡二叉树)中。每次调度,调度程序需要从树中找出优先级最高的进程。复杂度为O(logN)。
那么,为什么从linux 2.6早期到近期linux 2.6版本,调度程序选择进程时的复杂度反而增加了呢?
这是因为,与此同时,调度程序对公平性的实现从上面提到的第一种思路改变为第二种思路(通过动态调整优先级实现)。而O(1)的算法是基于一组数目不大的链表来实现的,按我的理解,这使得优先级的取值范围很小(区分度很低),不能满足公平性的需求。而使用红黑树则对优先级的取值没有限制(可以用32位、64位、或更多位来表示优先级的值),并且O(logN)的复杂度也还是很高效的。
调度触发的时机
调度的触发主要有如下几种情况:
1、当前进程(正在CPU上运行的进程)状态变为非可执行状态。
进程执行系统调用主动变为非可执行状态。比如执行nanosleep进入睡眠、执行exit退出、等等;
进程请求的资源得不到满足而被迫进入睡眠状态。比如执行read系统调用时,磁盘高速缓存里没有所需要的数据,从而睡眠等待磁盘IO;
进程响应信号而变为非可执行状态。比如响应SIGSTOP进入暂停状态、响应SIGKILL退出、等等;
2、抢占。进程运行时,非预期地被剥夺CPU的使用权。这又分两种情况:进程用完了时间片、或出现了优先级更高的进程。
优先级更高的进程受正在CPU上运行的进程的影响而被唤醒。如发送信号主动唤醒,或因为释放互斥对象(如释放锁)而被唤醒;
内核在响应时钟中断的过程中,发现当前进程的时间片用完;
内核在响应中断的过程中,发现优先级更高的进程所等待的外部资源的变为可用,从而将其唤醒。比如CPU收到网卡中断,内核处理该中断,发现某个socket可读,于是唤醒正在等待读这个socket的进程;再比如内核在处理时钟中断的过程中,触发了定时器,从而唤醒对应的正在nanosleep系统调用中睡眠的进程。
所有任务都采用linux分时调度策略时:
1,创建任务指定采用分时调度策略,并指定优先级nice值(-20~19)。
2,将根据每个任务的nice值确定在cpu上的执行时间(counter)。
3,如果没有等待资源,则将该任务加入到就绪队列中。
4,调度程序遍历就绪队列中的任务,通过对每个任务动态优先级的计算权值(counter+20-nice)结果,选择计算结果最大的一个去运行,当这个时间片用完后(counter减至0)或者主动放弃cpu时,该任务将被放在就绪队列末尾(时间片用完)或等待队列(因等待资源而放弃cpu)中。
5,此时调度程序重复上面计算过程,转到第4步。
6,当调度程序发现所有就绪任务计算所得的权值都为不大于0时,重复第2步。
所有任务都采用FIFO时:
1,创建进程时指定采用FIFO,并设置实时优先级rt_priority(1-99)。
2,如果没有等待资源,则将该任务加入到就绪队列中。
3,调度程序遍历就绪队列,根据实时优先级计算调度权值(1000+rt_priority),选择权值最高的任务使用cpu,该FIFO任务将一直占有cpu直到有优先级更高的任务就绪(即使优先级相同也不行)或者主动放弃(等待资源)。
4,调度程序发现有优先级更高的任务到达(高优先级任务可能被中断或定时器任务唤醒,再或被当前运行的任务唤醒,等等),则调度程序立即在当前任务堆栈中保存当前cpu寄存器的所有数据,重新从高优先级任务的堆栈中加载寄存器数据到cpu,此时高优先级的任务开始运行。重复第3步。
5,如果当前任务因等待资源而主动放弃cpu使用权,则该任务将从就绪队列中删除,加入等待队列,此时重复第3步。
所有任务都采用RR调度策略时:
1,创建任务时指定调度参数为RR,并设置任务的实时优先级和nice值(nice值将会转换为该任务的时间片的长度)。
2,如果没有等待资源,则将该任务加入到就绪队列中。
3,调度程序遍历就绪队列,根据实时优先级计算调度权值(1000+rt_priority),选择权值最高的任务使用cpu。
4,如果就绪队列中的RR任务时间片为0,则会根据nice值设置该任务的时间片,同时将该任务放入就绪队列的末尾。重复步骤3。
5,当前任务由于等待资源而主动退出cpu,则其加入等待队列中。重复步骤3。
系统中既有分时调度,又有时间片轮转调度和先进先出调度:
1,RR调度和FIFO调度的进程属于实时进程,以分时调度的进程是非实时进程。
2,当实时进程准备就绪后,如果当前cpu正在运行非实时进程,则实时进程立即抢占非实时进程。
3,RR进程和FIFO进程都采用实时优先级做为调度的权值标准,RR是FIFO的一个延伸。FIFO时,如果两个进程的优先级一样,则这两个优先级一样的进程具体执行哪一个是由其在队列中的未知决定的,这样导致一些不公正性(优先级是一样的,为什么要让你一直运行?),如果将两个优先级一样的任务的调度策略都设为RR,则保证了这两个任务可以循环执行,保证了公平。
Ingo Molnar-实时补丁
为了能并入主流内核,Ingo Molnar的实时补丁也采用了非常灵活的策略,它支持四种抢占模式:
1.No Forced Preemption(Server),这种模式等同于没有使能抢占选项的标准内核,主要适用于科学计算等服务器环境。
2.Voluntary Kernel Preemption(Desktop),这种模式使能了自愿抢占,但仍然失效抢占内核选项,它通过增加抢占点缩减了抢占延迟,因此适用于一些需要较好的响应性的环境,如桌面环境,当然这种好的响应性是以牺牲一些吞吐率为代价的。
3.Preemptible Kernel(Low-Latency Desktop),这种模式既包含了自愿抢占,又使能了可抢占内核选项,因此有很好的响应延迟,实际上在一定程度上已经达到了软实时性。它主要适用于桌面和一些嵌入式系统,但是吞吐率比模式2更低。
4.Complete Preemption(Real-Time),这种模式使能了所有实时功能,因此完全能够满足软实时需求,它适用于延迟要求为100微秒或稍低的实时系统。
实现实时是以牺牲系统的吞吐率为代价的,因此实时性越好,系统吞吐率就越低。
wait(linux kernel 等待队列)
Linux内核中的等待队列是同步和异步事件处理的常见工具。它在资源访问控制和事件通知中扮演着关键角色。想象一下,任务A想要对某个模块执行操作,但条件尚未成熟,这时任务A会通过等待队列进入休眠状态,直到条件满足。这时,模块会将A从队列中唤醒。
与信号量相比,等待队列提供了更多灵活性。虽然两者原理相似,涉及节点的申请和挂载,以及进程的挂起与唤醒,但等待队列允许自定义唤醒条件,可一次性唤醒多个进程,让它们各自检查条件是否满足。核心结构包括wait_queue_head作为链表头部,wait_queue_entry记录进程指针,等待队列头需要用户手动定义,而wait_queue_entry则在API中隐含处理。
快速使用等待队列的步骤包括:包含相关头文件,初始化等待队列头,资源访问者通过wait_event等待条件,提供者通过wake_up唤醒。例如:
初始化:#include"wait.h",init_waitqueue_head(wq_head)
等待:wait_event(wq_head, condition),等待条件成立
唤醒:wake_up(wq_head)
在源码中,可以参考kernel/sched/wait.c和include/linux/wait.h。了解更多细节,可以通过查阅5.16.5版本的git.kernel.org链接。下面是一些关键函数的源码剖析:
初始化等待队列头:主要设置锁和链表
wait_event的底层实现:涉及宏展开和唤醒回调函数
wake_up函数:执行唤醒任务,通常调用try_to_wake_up,涉及唤醒回调和通用唤醒任务处理
深入理解等待队列的运作,有助于我们更好地控制和管理内核中的并发行为。欲了解更多Linux内核源码解析,请关注我的专栏:RTFSC(Linux kernel源码轻松读)。