2008年4月16日星期三

Linux查看线程数量和线程的CPU百分比

调用命令:
ps H -eo user,pid,ppid,tid,time,%cpu,cmd --sort=%cpu |grep XXXX

UNIX中的同步服务(Synchronization services)

UNIX提供了基于POSIX标准的线程级同步单位,有些即使在不同进程中的线程之间也是可以使用的。同步服务包括了至少下表这些:

Synchronization service Supported between processes Supported across a UNIX LAN
Mutexes Yes No
Condvars Yes No
Barriers No No
Sleepon locks No No
Reader/Writer locks Yes No
Semaphores Yes Yes(Named only)
FIFO scheduling Yes No
Send/Receive/Reply Yes Yes
Atomic operations Yes No

以上的同步单位是直接通过内核实现的,除了:barries, sleepon locks以及reader/writer locks(这是使用mutexes和condvars构建的)、atomic operations(这可能是处理器直接完成的或在内核中模拟)。


Mutexes:相互排除锁定(mutual exclusion locks)

互排除锁定或互斥体(mutexes)是最简单的同步服务。互斥体用来保证线程间共享数据的排他性访问。一般在访问共享数据(通常为关键段)的代码附近互斥体被获得(pthread_mutex_lock()或pthread_mutex_timedlock())并释放( pthread_mutex_unlock())。

在任何时间只能够有一个线程锁定互斥体。试图锁定一个已锁定互斥体的线程将闭塞直到拥有该互斥体的线程释放该互斥体为止。当互斥体被释放时,等待锁定该互斥体的最高优先级线程将解除闭塞并拥有该互斥体。通过这种方式,线程依照优先级的次序顺序访问该关键区域。

在大多处理器上,互斥体的获得并不需要进入内核来获得一个空闲的互斥体。允许这么做的在x86处理器上是使用比较-交换操作码,在大多RISC处理器上是使用载入/保存条件操作码。

只有在互斥体被占时,线程在获取互斥体时完成进入内核的动作并进入闭塞列表;当其他线程等待对那个互斥体解除闭塞时会退出,内核进入动作也就结束了。这样,对于无竞争的关键段或资源的获得与释放就会很快,操作系统的工作就是解决竞争问题。

一个非闭塞锁定函数(pthread_mutex_trylock())可以用来测试互斥体当前的锁定状态。为了最佳的性能,关键段的运行时间应该短并且持续时间有限。当线程在关键段可能闭塞的话,就应该使用条件变量(condvar)。

互斥体与优先级继承(Priority inheritance)

默认情况下,当一个要获得互斥体的线程的优先级比该互斥体的当前拥有线程的优先级高的时候,拥有互斥体的当前线程的有效优先级就会自动提升到与这个 等待互斥体的被闭塞的较高优先级线程的优先级一致。当释放该互斥体的时候,该线程的优先级就会回复为其真实优先级。这个安排方式既能够保证较高优先级等待 互斥体的闭塞时间最短,又能够解决经典的优先级颠倒问题。

函数pthread_mutexattr_init()可以设置协议为PTHREAD_PRIO_INHERIT来允许这种动作,可以调用函数pthread_mutexattr_setprotocol()来修改这个设定。函数 pthread_mutex_trylock()并不改变线程的优先级,因为它不产生闭塞动作。

通过函数pthread_mutexattr_setrecursive()可以修改互斥体的属性,让其可以被同一个线程递归锁定。当一个线程调用一个可能会锁定这个线程已经锁定的互斥体的例程的时候,就可以使用这个函数。

注意的是:递归类型的互斥体不是标准的POSIX服务-它不能够与condvars一起使用。

Condvars:条件变量

Condvar又叫做条件变量,是用来在满足某些条件时在关键段解除线程的闭塞。这些条件可以是任意复杂并独立于条件变量的。但是,条件变量只能够与互斥锁一起使用来完成监视的工作。

条件变量支持3种操作:

  • 等待(pthread_cond_wait()
  • 信号(pthread_cond_signal()
  • 广播(pthread_cond_broadcast()

需要注意的是条件变量信号与POSIX信号是没有联系的。

下面是条件变量的一个简单应用:



pthread_mutex_lock( &m );
. . .
while (!arbitrary_condition) {
pthread_cond_wait( &cv, &m );
}
. . .
pthread_mutex_unlock( &m );

在上面的例子里面,现获取互斥体再测试条件。这就确保了只有这个线程能访问被测的条件。当条件满足,代码就在等待函数处闭塞直到其他线程对该条件变量发出信号或有广播动作。

这里的while循环有2条存在的理由:首先,POSIX不能保证伪唤醒不会发生(例如在多处理器系统);其次,当其他线程对条件进行修改后,还必须进行重新测试以确保所做的修改与我们的标准相符。相关的互斥体在等待线程闭塞并允许其他线程进入关键段的时候就通过函数 pthread_cond_wait()解锁了。

执行信号操作的线程会将条件变量队列中优先级最高的线程解除闭塞状态,而广播操作则会把等待该条件变量的全部线程解除闭塞状态。解除闭塞的最高优先级线程就会锁住相应互斥体,在这个线程处理完关键段之后必须要将互斥体解锁。

对条件变量的等待操作可以设置一个超时时间(pthread_cond_timedwait())。一旦等待时间超过超时时间,等待线程就会解除闭塞。

壁垒(Barriers)

壁垒是同步机制的一种,用来栏住同时工作的线程(例如在矩阵运算中的多个线程),强迫它们在某个特定点等待直到全部线程结束,之后其中的线程才能继续运行。

pthread_join()函数一直等待所有的线程结束,而壁垒的使用则是等待线程在某特定点集合(rendezvous)。当特定数量的线程到达壁垒时,就全部解除这些线程的闭塞让它们继续运行。

壁垒的创建使用函数pthread_barrier_init()


#include
int pthread_barrier_init (pthread_barrier_t *barrier,
const pthread_barrierattr_t *attr, unsigned int count);


这个函数在传入的地址(指向壁垒对象的指针在参数barrier中)创建一个壁垒对象,并按照attr中的属性对该对象进行设定。参数count为必须调用函数pthread_barrier_wait()的线程数量。

一旦创建了壁垒,每个线程必须调用函数pthread_barrier_wait()来表明该线程已经完成:

#include 

int pthread_barrier_wait (pthread_barrier_t *barrier);

当线程调用函数pthread_barrier_wait()之后,它就进入闭塞状态,直到pthread_barrier_init()函数中线程数量参数中所设定的这么多线程调用了pthread_barrier_wait()函数后才解除闭塞状态。当调用 pthread_barrier_wait()函数的线程的数目正确,这些线程就会同时解除闭塞。

下面是例子代码:

/*
* barrier1.c
*/

#include
#include
#include
#include

pthread_barrier_t barrier; // barrier synchronization object

main () // ignore arguments
{
time_t now;

// create a barrier object with a count of 3
pthread_barrier_init (&barrier, NULL, 3);

// start up two threads, thread1 and thread2
pthread_create (NULL, NULL, thread1, NULL);
pthread_create (NULL, NULL, thread2, NULL);

// at this point, thread1 and thread2 are running

// now wait for completion
time (&now);
printf ("main() waiting for barrier at %s", ctime (&now));
pthread_barrier_wait (&barrier);

// after this point, all three threads have completed.
time (&now);
printf ("barrier in main() done at %s", ctime (&now));
}

void *
thread1 (void *not_used)
{
time_t now;

time (&now);
printf ("thread1 starting at %s", ctime (&now));

// do the computation
// let's just do a sleep here...
sleep (20);
pthread_barrier_wait (&barrier);
// after this point, all three threads have completed.
time (&now);
printf ("barrier in thread1() done at %s", ctime (&now));
}

void *
thread2 (void *not_used)
{
time_t now;

time (&now);
printf ("thread2 starting at %s", ctime (&now));

// do the computation
// let's just do a sleep here...
sleep (40);
pthread_barrier_wait (&barrier);
// after this point, all three threads have completed.
time (&now);
printf ("barrier in thread2() done at %s", ctime (&now));
}

主线程创建壁垒对象并用一个线程的数目来初始化该对象,当达到数量的线程通过壁垒同步后才能够继续运行。在上面的例子,初始化的线程总数为3:一个用于main()线程,一个用于thread1(),一个用于 thread2()

在初始化后,启动线程thread1()thread2()。为了简单起见,这里让线程进入sleep状态来产生延迟,模拟正在计算的情况。为了同步,主线程在壁垒闭塞了自己,只有在其他两个线程也在壁垒处闭塞之后,才能够解除线程的闭塞。

这里使用了的壁垒函数包括了:

函数 描述
pthread_barrierattr_getpshared() 获取壁垒进程共享属性值
pthread_barrierattr_destroy() Yes
pthread_barrierattr_init() No
Sleepon locks No
Reader/Writer locks Yes
Semaphores Yes
FIFO scheduling Yes
Send/Receive/Reply Yes
Atomic operations Yes


UNIX环境中的IPC问题

| | Comments (0) | TrackBacks (0)

由于进程中的所有线程能够随意访问共享数据空间,是不是这样就能解决所有的IPC问题?能不能通过共享内存来做数据的通讯而无需其他的运行模式或者是IPC机制?

事实上没那么简单!

一个问题是,多个线程各自对公有数据的访问必须同步。 一个线程对数据修改的同时另外一个线程对该数据进行读取会读到不一致的数据,这种做法会导致灾难。例如,一个线程更新链接列表的时候,就不能够允许其他线 程来遍历或者是修改这个列表,只有在第一个线程完成操作之后才能够做进一步的操作。以这种方式必须“串行”(一次只能有一个线程访问)运行的代码段称之 “关键段”(critical section)。如果没有一种同步机制来保证对这个列表的串行访问,这个程序将会发生故障(间歇性的,视这种“撞击”发生的频率而定)并会不可修复的破 坏这些列表。

互斥体(mutex)、信号(semaphore)以及条件变量(condvar)就是用来解决这种问题所使用的同步工具。

尽 管同步机制能够让线程协同工作,但是共享内存本身并不能解决IPC的问题。例如,线程之间是可以通过共享数据空间通讯,但是这只对同一进程之内的线程有 效。如果应用程序需要对数据库服务器进行查询该怎么办?首先要把查询的详细信息传送给数据库服务器,可是要通信的线程处于数据库服务器进程中而那个服务器 的地址空间对我们说是不能访问的。

操作系统来处理这种分布式网络的IPC问题,因为消息传递接口在本地以及远程网络环境下是工作的,并 可以用来访问全部的操作系统访问。由于消息的大小可以准确计量并且一般都很小(例如一个写请求的错误状态或者是一个小小的读请求),通过网络使用消息传递 数据可能只用4K的页面空间,这比使用分布网络共享内存要小很多。

什么时候做调度的动作?

当有内核调用、例外事件发生或者是硬件中断的时候,微内核就会进入运行中的线程并将该线程暂时暂停。当任何线程的运行状态发生改变的时候,就会有线程调度的动作,这个动作与线程属于那个进程无关。线程是跨越全部进程进行全局调度的。

一般情况下,暂停的线程会再继续运行的,但是线程调度器在正运行的线程有以下三种情况时会执行从一个线程到另一个线程的切换:被闭塞(blocked)、被高优先级线程抢占(preempted)和放弃执行(yields)。

当线程被闭塞(blocked)

闭塞(blocked)的情况一般发生于运行中的线程必须等待某些事件的发生(例如响应IPC的请求、等待线程同步中的互斥体(mutex)等等)。线程 闭塞后就会被从就绪队列中移除,就绪队列中最高优先级的线程开始运行。当被闭塞线程解除闭塞时候,就会被放入同优先级的就绪队列的尾端。

当线程被抢占(preempted)

一旦就绪队列里被置入更高优先级线程,当前运行线程就会被抢占(当更高优先级线程结束后,这个线程会恢复就绪状态)。被抢占的线程会被放到同优先级的线程就绪队列的开始,较高优先级的线程开始运行。

当线程放弃(yield)

当运行中的线程自愿放弃处理器(sched_yield())时就会被放到同优先级线程就绪队列的尾端。之后,最高优先级的线程开始运行(也有可能这个线程就是刚刚放弃的那个线程)。

线程调度中的优先级

每个线程都有自己的优先级。调度器通过查看每个就绪的线程的优先级来挑选下一个要运行的线程。所谓的就绪就是能够使用CPU。最高优先级的线程被选中开始运行。

image

上图中,线程就绪队列中有6个线程(A-F)已经就绪。其他的线程(G-Z)已经闭塞。现在线程A正在运行。A、B和C处于最高优先级,所以它们将按照运行的线程调度算法分享处理器。

操作系统一共支持256个调度优先级。一个非根(non-root)线程的优先级可以设为1到63(最高优先级),这是独立于调度策略的。只有根线程 (root threads),就是有效uid为0的线程,可以设置高于63的优先级。特别的空闲(idle)线程(在进程管理器中)的优先级为0,并且可以随时运 行。一个线程默认是继承它的母线程的优先级。

对于非根(non-root)进程可以使用procnto -P参数来设置它所允许的优先级范围:

procnto -P priority

下表归纳了优先级范围:

优先级 拥有者Owner
0 空闲线程(Idle thread)
1到priority-1 非根或根(Non-root or root)
priority到255 根(root)

需要注意的是,为了防止优先级反转的情况,内核可能会临时提升线程的优先级。

线程就绪队列中,线程是按照优先级进行排序的。线程就绪队列实际上是由256个独立的队列所组成的,这256个独立的队列每个代表一个优先级。大多情况 下,线程在它们的同优先级队列中是以先入先出(FIFO)的顺序排列的。最高优先级的队列中的第一个线程首先执行。(FIFO的一个例外是,一个刚接收到 客户端消息并从RECEIVE-blocked状态退出的服务器线程,这个服务器线程被放到同优先级队列的第一个位置,这个顺序就不是先入先出 (FIFO),而是后入先出(LIFO))。

线程调度算法(Scheduling algorithms)

基于应用场合的不同,UNIX提供了以下几种调度算法:先入先出调度(FIFO Scheduling)、循环调度(round-robin scheduling)以及偶发调度(Sporadic Scheduling)。

系统中的线程可能以任一种方法运行。这些方法在基于每个线程的基础上是有效的,并不是基于节点上所有进程和线程的全局为基础的。

先入先出(FIFO)和循环(round-robin)调度算法只用于2个或更多拥有相同优先级的线程就绪的情况。这种情况下这些线程直接与对方竞争。偶发调度则是线程运行的所占用资源的调整。任何情况下,当更高优先级线程就绪后,就会立即抢占所有的较低优先级线程。

image

上图中,3个线程为同一优先级并且全部就绪。如果线程A被闭塞,线程B将运行。

尽管线程从它的母进程那里继承了调度算法,但是线程可以要求改变内核所给与的调度算法。

先入先出调度(FIFO Scheduling)

在先入先出调度算法中,线程被选择出来连续运行直到它:自愿放弃控制(例如被闭塞)或被更高优先级线程抢占。下图为先入先出调度(FIFO scheduling)的示意图:

image

循环调度(Round-robin scheduling)

在循环调度算法中,线程被选出连续运行直到它:自愿放弃控制或被更高优先级线程抢占或者是耗光分配给它的时间片(timeslice)。如下图所示,线程A运行到耗尽了它的时间片,下一个就绪的线程(线程B)开始运行:

image

所谓时间片(timeslice)是指派给每个进程的时间单位。一个线程耗光它的时间片之后,它就会被抢占,同优先级的下一个就绪线程就得到控制权。一个时间片是时钟周期的4倍。

可以说除了时间片之外,循环调度与先入先出调度是一样的。

偶发调度(Sporadic scheduling)

偶发调度算法一般用于在设定的时间段里限制线程运行的最长时间。当一个系统同时处理周期性和非周期性事件,对其进行速率单调性分析(Rate Monotonic Analysis(RMA))时这个偶发调度算法是必须的。基本上,这个调度算法可以让线程在不危害系统中其他线程和进程的硬实时性能的同时处理非周期性 事件。

与先入先出调度(FIFO scheduling)类似,使用偶发调度的线程在被闭塞或者是被更高优先级线程抢占之前是一直运行的。与自适应调度类似,使用偶发调度的线程能够降低优先级,但是通过使用偶发调度可以对线程的动作有更精确的控制。

使用偶发调度,线程的优先级可以在前台(正常优先级)和后台(低优先级)之间动态变换。使用以下参数,可以控制这种偶发变换的状态:

初始预算(C)(Initial budget)

线程在没有降低到低优先级(L)之前,在正常优先级(N)所允许运行的总时间。

低优先级(L)(Low priority)

线程要降低到的低优先级的优先级水平。线程在该优先级运行时为后台运行,当在正常优先级运行是为前台运行。

补偿周期(T)(Replenishment period)

线程被允许消耗其预算时间的这段时间段。为了调度补偿操作,POSIX系统同样使用这个参数作为线程就绪后的时间偏移量。

最大待补偿数(Max number of pending replenishments)

这个参数限制执行补偿操作的次数,从而也限制了由于使用偶发调度所消耗的系统开支总量。

在一个配置较差的系统中,一个线程的运行预算可能会由于过多的闭塞而侵蚀,也就是说它得不到足够多的补偿(replenishment)。

image

如上图所示,偶发调度首先设定线程的初始运行时间预算(C),这段时间由该线程消耗并周期性的补偿(在时间总数到达T时)。当线程闭塞,运行预算中所消耗的时间总数(R)将在线程就绪之后安排到在晚些时候(例如在第40毫秒)进行补偿。

在正常优先级N下,线程运行的时间为它的初始运行预算时间C。当这段时间耗尽,该线程的优先级降为它的低优先级L直到补偿操作为止。

例如,假想一个系统中的线程永不闭塞或者是被抢占,如下图所示:

image

这 里,线程将降低到低优先级(后台运行),在后台它是否有机会运行依赖于系统中的其他线程的优先级。当补偿操作动作时,线程的优先级上升到其原有水平。这就 保证了,在合理配置的系统中,每个线程都有机会每隔一个周期T能够最多运行C这么长的时间。这就保证了在优先级N运行的线程只消耗了C/T比例的系统资 源。

当一个线程闭塞多次的话,就有可能会有多个补偿操作在不同的时间启动。这就意味着,线程的在间隔T的时间内的总运行时间预算为C,但是在这段时间之内的运行可能并不是连续的。

image

就如上图所示,该线程在每个长度为40毫秒的补偿周期(T)里面的运行预算时间(C)为10毫秒。

  1. 线程的首次运行在3毫秒后闭塞,一个3毫秒补偿操作就被调度到在40毫秒的时标处开始,也就是说在第一个补偿周期结束后开始;
  2. 这个线程在6毫秒后又得到运行的机会,这又标志着一个新的补偿周期(T)的开始。而线程还剩下7毫秒的运行预算时间;
  3. 线程没有被闭塞,用光它的剩余运行预算时间7毫秒,之后优先级降为低优先级L,这时它可能运行也可能不运行。这样一个7毫秒的补偿就被调度到在46毫秒(40+6)处开始,也就是说等待T结束后开始;
  4. 线程在40毫秒处有一个3毫秒的补偿操作(见步1),这是回复其原正常优先级;
  5. 线程使用了其3毫秒的运行预算时间就降为其低优先级;
  6. 线程在46毫秒处的补偿操作有其7毫秒的运行预算时间(见步3),重新升为其正常优先级。

之后就如此循环往复。线程将在它的两个优先级之间切换,在系统中以这种可预见的方式处理非周期性的事件。

控制优先级以及调度算法

线程的优先级在其运行期间是可变化的,这个变化可能是由线程自己控制的也有可能是当系统内核接收到较高优先级的消息而调整的。

除了优先级,还可以设定内核用于线程的调度算法。下表为POSIX标准中执行这些操作的调用以及这些库例程所使用的内核调用:

POSIX call Microkernel call 描述
sched_getparam() SchedGet() 获得调度优先级
sched_setparam() SchedSet() 设置调度优先级
sched_getscheduler() SchedGet() 获得调度策略
sched_setscheduler() SchedSet() 设置调度策略
以上文章摘抄自 http://www.eggheader.com/

2007年2月25日星期日

Linux动态库实验-多重符号的定义

主要目的:
  • 对Linux下so动态库的编程中,内部符号是否可以允许多重定义。
  • 在Windows 的Dll中,如果一个符号没有导出,那么在进行链接的时候是不会引起错误的。
  • 当使用so动态库时会不会有这样的问题呢?
  • 测试重点:在库的static 和 extern 的定义问题
测试范围:
  • 变量
  • 函数
测试结果:
  • 所有没有使用static 定义的符号均被导出,可以被宿主程序进行调用
  • 当没有使用static 或者使用extern标识的导出符号与宿主程序中的符号定义一致时,动态库中的符号被宿主程序所替代,甚至包括在动态库内部的调用,例如:在动态库内有一个函数调用导出函数............
  • 很有趣,是否和多重继承很像哦