ePass1knd代码阅读体会 (II)

发布时间:2010-2-12 10:36
分类名称:Private


ngslotd部分类静态结构

ePass1knd代码阅读体会 (II) - Dsliu - Dspace

ngCSlotMgr和ngCSlot各自从ng_SlotBase派生,ng_SlotBase本身扮演了数据处理器的角色,从客户端接收请求,处理, 发送结果给客户端。他声明了三个按顺序执行的模板方法1)_Recv,_ProcessRequest,_Send 组成数据处理的一整个流程,具体的数据处理方法_ProcessRequest由子类实现.

ngCSlotMgr处理全局性的命令,ngCSlot处理针对本slot的命令.他们通过父类的函数向IPCServer注册自己并表明自己感兴趣的事 件后挂起,当出现此事件后IPCClient激活对应的线程以处理此请求. 当接收到操纵硬件的请求时,ngCSlot通过ngTSP作为代理操纵硬件. ngCSlotModule始终在等待硬件插入拔出的消息.

监控程序启动序列图

ePass1knd代码阅读体会 (II) - Dsliu - Dspace

并发与IPC机制

ePass1knd并发实现示意图

ePass1knd代码阅读体会 (II) - Dsliu - Dspace

ePass1knd中涉及到IPC的可以看作3个互相协作的部分,客户端部分,服务器部分和共享内存部分:

共享内存

ePass1knd代码阅读体会 (II) - Dsliu - Dspace

如上所述,共享内存分为两部分,在服务器端看来,分别是读取通道和发送通道.每个通道拥有两个event以实现同步:

并发机制分析

采用多线程原因

为避免和其他任务发生冲突
为简化应用程序设计

将应用分解为各个独立的逻辑任务

操作系统的线程模型

N:1

此模型为用户线程,线程在进程范围内争抢cpu,同一个进程的线程不可以调度到不同CPU上,同一个进程的某个线程阻塞会阻塞住整个进程。 (HP-UX10.20,SunOS4.x)

1:1

此模型为核心线程,线程在系统范围中抢cpu,同一个进程的线程可调度到不同CPU上,同一个进程的某个线程阻塞不会阻塞住整个进程。 (W2k,Linux,HP-UX11)

N:M

以上两种模型结合,创建线程时可由用户指定采用哪种模型。 (Solaris)

线程创建策略

Eager spawning

饥饿式创建。程序启动时,预先创建多个线程形成线程池(pool),可以改善响应时间,但启动速度慢。适用于线程创建和销毁频繁的大规模并发应用中。

On-demand spawning

按需创建,启动速度快,单个响应速度慢。适用于线程创建和销毁不很频繁的应用。

线程使用

一个客户一个线程
一个请求一个线程

线程生存期操作

自愿终止

线程通过正常的执行流程返回,可以正常回收系统资源。

无意终止

被其他线程强行中止,系统资源不能回收,对7*24运行的系统来说,造成的泄漏是可观的。

ePass1knd并发分析

在一个多线程应用中究竟选取哪种策略,跟它被使用的方式密切相关。一边是高效率,一边是实现难度,必须小心的选取二者之间的平衡点,使效率可接受且实现起 来较为容易。广泛且精确的度量影响整个系统设计的因素,据此来做具体实现。比如,一个用户拥有多个token的几率有多大?多个应用程序同时使用一个 token的几率有多大?读写频繁吗,读与写的次数之间可比拟吗?等等问题,需要根据用户群,和用户的使用习惯来确定。

在设计软件架构时,做适当的封装,不管对每个程序员的日常开发的方便(手边有了内容丰富的工具箱,问题更局部,可将注意力集中于业务上而不用考虑很多琐碎 的非常细节的东西),还是对新员工的快速上手(如果封装的好的话,他只需要了解他所需要了解的那部分模块,即可快速投入工作),更重要的是对客户的需求变 更的反应,都是非常方便的。

纵观整个ePass1k应用,只有硬件key是需要序列化访问的。而对key的访问基本上就是费时的IO操作,故我们系统合理的采用一个key一个线程的 实现方式,在一个线程中序列化访问key,而key的争用问题自然而然的解决了。

来看看ePass1k实现:

ePass1knd代码阅读体会 (II) - Dsliu - Dspace

可以发现,资源的争用主要发生在:

读取方面,由于有明确的目标(正向是服务器,反响是确定的某个客户线程)故没有争用情况,只要处理好数据同步即可。

我们来观察一次请求的处理过程来仔细了解共享内存的争用:

Recv部分共享内存:

  1. 客户机想要发送一个请求,先请求共享内存的Recv部分的互斥锁
  2. 客户机等待服务器取走上一次交互的数据
  3. 获取到权限后,Recv部分被此客户机线程所独享,客户机写完后释放并通知服务 器读取请求
  4. 服务器接到通知后通知并等待处理线程取请求,此内存一直被占用到处理线程完成手 头工作并取走请求

Send部分共享内存:

  1. server获取Send部分共享内存的写权限
  2. server写入数据
  3. 通知对应的client取数据
  4. client取完数据交互完成

综上,多方写入需互斥,而读取是反应式的(得到通知后只有一个线程在读取),不用互斥。对共享内存的争用是ePass1k中的”非功能性”限制,即与功能 无关,只与实现方式相关.而对硬件的序列化访问是”功能性”限制.一般而言,软件设计时应尽量减少”非功能性”限制.

可能的改进

本着专门事情专门模块来做的原则,可以做以下由低到高几个层次的封装,提供较高层次的抽象,软件构造时更方便,且在此基础上的交流更容易。使类更内聚(功 能更单一),使类变成某一方面的“专家”,将事物分解成各个方面的问题并交给“专家”去处理。

<

锁的封装

可以将各种锁封装一下,作为多线程开发的基本工具,一次封装到处使用。

各种锁

线程锁和进程锁
SpinLock

自旋锁的名字来自它的特性,在试图加锁的时候,如果当前锁已经处于”锁定”状态,加锁进程就进行”旋转”,用一个死循环测试锁的状态,直到成功的取得锁。 自旋锁的这种特性避免了调用进程的挂起,用”旋转”来取代进程切换。而我们知道上下文切换需要一定时间,并且会使高速缓冲失效,对系统性能影响是很大的, 所以自旋锁在多处理器环境中非常方便。当然,被自旋锁所保护的”临界代码”一般都比较短,否则就会浪费过多的CPU资源。自旋锁的效率远高于互斥锁。

Self-recursiveLock

一个线程多次获取同一个锁,不会被锁死

ConditionLock

线程可以调整和调度自己的处理过程,Pthread和Solaris之上的UNIX Iternational(UI)线程库本身支持,W32不支持需模拟.

Readers/WriterLock

(可以在ePass1knd的广播时使用)

锁使用方式

ScopedLock

代码中有定界加锁类,而且不止一个,不如写成一个模板:

/**
* 定界加锁模板类
* @remark
* 锁类型作为模板参数,在对象生存期自动处理锁的Lock和Unlock,要求锁的Lock和Unlock为const成员
* @see
*
*/

template<class T>
ScopedLock
{
public:
/**
* 构造函数,自动调用锁的lock
* @param lock 已创建的锁实例,必须是T类型或能隐式转换为T
*/

ScopedLock(T const & lock):lock_(lock)
{
lock_.Lock();
}
 
/**
* 析构函数,自动调用锁的Unlock
*/

~ScopedLock()
{
lock_.Unlock();
}
private:
/**
* 不允许默认构造,依照"对象创建即初始化"原则,不传入锁的实例没有任何意义
*/

ScopedLock();
 
/**
* 不允许复制,在复制时应该加锁,但考虑到大部分情况锁类型不是self-recursive锁,
* 一旦拷贝即造成死锁,为防止用户使用错误故此特性不支持.
*/

//@{
ScopedLock(ScopedLock const &);
ScopedLock& operator=(ScopedLock const &);
//@}
private:
T const & lock_;
};
 
/**
* 定界加锁对self-recursive锁的全特化,支持拷贝构造
*/

template<>
ScopedLock<SELF_RECURSIVE_LOCK>
{
public:
/**
* 构造函数,自动调用锁的lock
* @param lock 已创建的锁实例,必须是T类型或能隐式转换为T
*/

ScopedLock(SELF_RECURSIVE_LOCK const & lock):lock_(lock)
{
lock_.Lock();
}
 
/**
* 析构函数,自动调用锁的Unlock
*/

~ScopedLock()
{
lock_.Unlock();
}
 
/**
* 拷贝构造
*/

ScopedLock(ScopedLock const & src):lock_(src.lock_)
{
lock_.Lock();
}
 
 
 
private:
/**
* 不允许默认构造,依照"对象创建即初始化"原则,不传入锁的实例没有任何意义
*/

ScopedLock();
 
/**
* 只允许拷贝构造,不允许赋值
*/

ScopedLock& operator=(ScopedLock const & src);
private:
SELF_RECURSIVE_LOCK const & lock_;
};
StrategyLock

实现策略化加锁,可以随时替换锁类型,在定义需要加锁的类时(例如实现共享内存类),可以:

template <typename LOCK_TYPE>
ClassNeedLock
{
/* Other members are ommited*/
private:
LOCK_TYPE lock_;
};
/**
* 所有锁的父类,提供通用接口
*/

class Lock
{
public:
virtual void Lock()=0;
virtual void Unlock()=0;
};
 
/**
* 一个具体锁,实现Lock的接口
* @see Lock
*/

class ConcreteLock:public Lock
{
public:
virtual void Lock();
virtual void Unlock();
};
 
/**
* 使用锁的类
*/

ClassNeedLock
{
public:
ClassNeedLock(Lock* lock);
/* Other members are ommited*/
private:
Lock* plock_;
};

多态锁不推荐,因为如果这样做,锁是由外部创建并传入的,将锁暴露了

流量控制

把流量控制功能集中到一个类中,提供缓存,对要传输的数据进行分组,组合,以及避免发送过快淹没接收方。甚至在需要的时候可以对数据进行某些处理(如加 密)。

通讯通道的封装

把通讯通道进行封装,外部可指定锁的策略,流量控制策略,以及来信触发机制等。

另外,对通讯通道作封装后可方便的改变通讯方式,如把共享内存方式改为loop back socket(win32平台下有些杀毒软件会使用Layered Service Provider-LSP来对socket通讯数据进行截获,需要设置)或其他更适合ePass的进程间通讯方法.

多线程使用封装

我们程序中大量使用了线程特性,如p11和slotd中使用线程的方式基本类似,可将其实现为共享库供使用者调用,而不是每个模块中都重写一遍。

简化通讯方式

如上文提到的,我们应尽量避免“非功能性”限制,可以将实现改为如下图所示:

ePass1knd代码阅读体会 (II) - Dsliu - Dspace

客户端争用共享内存相当于争抢token(硬件的序列化访问是功能性需求),共享内存不用分收和发两个区了(因为收完了才能发,对一个key来说两个事件 不会同时发生),每个token分一页4k,1M内存能撑住200个epass(1M内存多吗?再说谁同时用200个….)

在客户端看来形成一种抽象:

ePass1knd代码阅读体会 (II) - Dsliu - Dspace

1) 模板方法模式: 父类将一个流程分成几个步骤,每个步骤声明为一个虚函数由子类具体定义,父类按流程顺序调用这些虚函数,形成处理数据的流程框架(或模板). 此模式为GOF经典设计模式中的一种.