第十三章 进程、线程类的实现
多线程是指在一个进程内可以同时运行多个任务,每个任务由一个单独的线程来完成。线程是进程运行的基本单位,一个进程中可以同时运行多个线程。如果程序被设置为多线程方式,可以提高程序运行的效率和处理速度。 多个线程共用一个进程的资源;进程的调度、切换是在10ms的“时钟滴答”定时中断程序里进行。如果一个线程获得CPU,那么在下一个Tick到来前;是不可能被切换出去的,除非自己放弃CPU。 虽然都是抢先式调度,但进程是动态优先级抢先式调度;而线程主要是固定优先级,加上消息先到优先的抢先式调度;用户进程的线程调度还包含消息驱动的事件过程处理。进程调度、线程调度都是在“时钟滴答”定时中断程序中进行,最终运行的还是进程内的线程;2种调度都有禁止抢先模式。当一个进程要对进程共享资源或线程要对进程内的共享资源进行操作时,各自设置为禁止抢先模式;直到完成后释放该模式。这类似一个同步锁;在禁止抢先模式时的“时钟滴答”定时中断程序是不会调度的,在下一“时钟滴答”定时中断如判断该模式解除,才会进行调度。要注意,进程的禁止抢先模式,只是不做进程调度吧;并不代表不做线程调度;反之,也一样。注意:进程锁最多只有X(系统参数、默认10)个“时钟滴答”,超过会被系统自动解锁;当进程的时间片用完,如果设定进程锁也一样被解除。应该编写公平的程序,锁住资源就要用完就解锁;让其它进程或线程或对象有机会运行。系统需要监控那些进程或线程或对象在锁住资源;这将反映在日志进程。如果一个线程要操控本进程的公共资源,那么它应该调用系统方法Key_Thread(),锁住线程调度;类似,如果要操控所有的进程的公共资源;那么它应该调用系统方法Key_Process(),锁住进程调度;系统方法都是“原子”的。
一、进程类 Process_main()是进程调度时的进程代码入口;所有的线程都是共用进程的属性表。进程有2种模式:无 线程的过程模式,线程模式。系统有这2种类,用户进程隐含的继承其中的一个类。这2种系统类都有不相同的Process_main()主方法,还有相同的一些简单的、不到1E的方法:这些方法将在后面或第二章修改版介绍。 Process_wait(); 保存优先级计数后清0、等待阻塞返回。PWRET Process_sleep(); 保存优先级计数后清0、睡眠阻塞返回。PLRET Process_yield(); 优先级计数清0、在该调度周期让步给其它进程;阻塞返回。PYRET Process_pend(); 保存优先级计数后清0、进程调度、返回到高优先级进程。PPRET Process_stop(); 进程终结返回,资源回收、进程撤销。PSRET Process_DispatchMessage();处理信号、进程的系统消息。 Process_init(); 进程共用部分的初始化 Process_PostMessage(); 向线程消息缓冲区添加消息、并启动相应线程。 Process_SendMessage(); 直接翻译和调用消息处理。 Send_Message(); 发送一条消息到用户、或内核、或外部的系统消息队列环。 Key_Process(); 锁住进程调度。 Clr_key_Process(); 完成共享资源操作后进程解锁。 Get_vnode_add(_Process/_file, pid/file_num);获得进程(文件)v节点指针的方法。 vnode_get(属性变量名); 获得v节点内的各个属性变量,如uid、gid等等。 vnode_set(属性变量名,数值); 设置v节点内的各个属性变量,如uid、gid等等。 v节点是文件i节点和目录项在内存中的结合体。 1、进程的过程模式类: 所有的这种类的进程都是共有同一个系统主方法,系统会根据pid调用相对应的用户main();方法。Pid的最高位为1,表示是进程的第一次运行;次高位为1就是过程类进程,为0是线程类进程。 class Process_pro{ // pid的低13位是进程号。 Process_main(pid){ // 进程的过程模式类主方法;占用:7W。 Process_init();// 进程共用部分的初始化,进程的第一次运行时需要。 LOOP: H2 = H12; // 从本地总线硬件控制器读入一条本进程的消息。 BT0 H2.3.W.31, LO1; // 用户消息跳。 Process_DispatchMessage();// 系统方法;处理信号、进程的系统消息。 JMP LOOP; // 处理下一条消息 LO1: main(); // 执行用户编写的进程消息处理代码、用户代码入口;用户类。 JMP LOOP; // 无条件跳处理下一条消息,循环。 } Process_XMUB{ // 进程小模式属性表; 256E 系统管理区域。 BU248E dx_table{ // 对象头列表,成为当前进程时,基址在A0寄存器。 // 992个对象号,前面160个是只读;后面832个是可读、写。16位对象号的 // 高6位是对象操作标志,低10位才是对象号。 BU2W [32] lf_tab; // 类方法表,0是系统类、1是本类、2-31是方法库DLL。 BU2W [128] thread_lf_tab; // 线程类方法表,128个线程组run()入口和长度。 // 线程号的高7位就是所属的线程类号,如果你只有一个线程类、64K个同类线程; // 那系统会把线程类表的内容设为同一个指向线程类run()的指针和类的方法长度。 // 过程模式的进程类不使用,这些项为空、0。 BU2W [64] dx_tab; // 对象表,dx_tab.63是除用户定义的dx_tab外、剩余的 // 对象全部由编译器打包形成。你可以每一个线程类,设一个根对象dx_tab.i;把 // 所有属于该类的线程中的对象属性表打包在一起。编译器就会设这些对象属性表是 // 相对于根的地址;操作是dx_tab.i.对象名.变量名。为节省资源,我们不可能设 // 置太多的对象号。编译器把dx_tab.i翻译成对象号、而对象名.变量名为偏移地址。 // dx_tab.i的别名是Di、dx_tab.63的别名为D。 BU2W [768] gx_tab; // 共享(动态)对象表,对应于一些动态分配、释放的对象。 } BU256 signal; // 256个信号位图。 BU256 [3] gx_no_WT; // 768个公共动态共享对象号位图变量。 BU1K sblocked; // 屏蔽码(对应信号、动态变量、对象位图)。 } }; 代码中、打开和操作一个文件,你必须先声明一个file属性表;包含日志文件服务进程返回的文件号、动态或静态对象号、文件内容(或是动态对象)的位容器、文件位置等等变量。 file{ BU32 fd; // 文件描述符(file descriptors)。文件号 BU48 f_pos; // 在磁盘文件中的当前位置。 BU16 dx_no; // Di或gx_tab.i 和流容器对象的操作标志,读、写等。 BUxE f_stream; // 文件流的x行位容器。当发消息给日志文件服务进程时,要提交 // 对象号..file.f_stream.W的相对偏移地址,和f_stream.len.W的流容器大小。 }
系统最大能打开1M个文件,每个进程最大打开的文件数为64K个。gx_no_WT,768个公共动态共享对象;主用于进程间的通讯。在APO中,这种对象是大量使用的;谁申请的、谁释放。共享对象名字gx_tab.i,是所有进程都共用的名字;但用户进程的共享对象要被系统安装了才能使用;要是进程申请的可以用,或有另一个进程提交的才行。否则,就会是空对象操作异常中断、被灭。对象头列表中的内容是系统安装和释放的。 用户系统消息队列,是一个16K条消息的环。属于当前用户进程的、可能是0.1%吧;如果用户进程处理一条消息的平均时间是20ns,那么循环一圈需要320ns。所有的用户进程都是共用一个用户系统消息队列。每读入一条用户消息,用户系统消息队列中的相应位置被清0,以便系统可以在那写入一条新消息;同时、找到下一条用户消息,这都是硬件实现的。用户程序不能读取一个系统消息,否则用户操作“系统变量”出错、被灭。但用户可以处理一条系统读入到H2行寄存区的用户消息。用户主要是编写main();里的代码和声明。main()就是类似那个 public static void main(String args[]){….},你懂的。 2、进程的线程模式类: 所有的这种类的进程都是共有同一个系统主方法,系统会根据pid调用相对应的用户main();方法。Pid的最高位为1,表示是进程的第一次运行;次高位为1就是过程类进程,为0是线程类进程。 class Process_pro{ // pid的低13位是进程号。 Process_main(pid){ // 进程系统方法;占用:11W。 Process_init(); // 进程共用部分的初始化 LOOP: Thread_DD();// 线程调度 32ns--452ns LOOP1: H2 = H12; // 从本地总线硬件控制器读入一条本进程的消息。 JZ LOOP; // 处理完一圈中的所有消息跳 BT0 H2.3.W.31, LO1; // 用户消息跳。 Process_DispatchMessage();// 处理信号、进程的系统消息。 JMP LOOP1; // 处理下一条消息 LO1: BT0 H2.3.W.30, LO2; // 立即消息处理跳。 Process_PostMessage();// 向线程消息缓冲区添加消息、并启动相应线程。 JMP LOOP1; // 处理下一条消息 LO2: Process_SendMessage(); // 直接翻译和调用消息处理。 JMP LOOP1; // 处理下一条消息 } Process_XMUB{ // 进程小模式属性表; 1KE 系统管理区域。 BU248E dx_table{ // 对象头列表,成为当前进程时,基址在A0寄存器。 // 992个对象号,前面160个是只读;后面832个是可读、写。16位对象号的 // 高6位是对象操作标志,低10位才是对象号。 BU2W [32] lf_tab; // 类方法表,0是系统类、1是本类、2-31是方法库DLL。 BU2W [128] thread_lf_tab;// 线程类方法表,128个线程组run()入口和长度。 BU2W [64] dx_tab; // 对象表, BU2W [768] gx_tab; // 共享(动态)对象表。 } BU256 signal; // 256个信号位图。 BU256 [3] gx_no_WT; // 768个公共动态共享对象号位图变量。 BU1K sblocked; // 屏蔽码(对应信号、动态变量、对象位图)。 BU64K Thread_WT;// 64K个线程优先级位图。 BU64K tblocked; // 线程屏蔽码。 BU64K Thread_RUN_WT;// 线程运行位图,Thread_WT BIC tblocked后的结果。 } }; 二、线程类 Thread_main(){ // 主线程系统方法;占用:2W,1E。线程号0、最高优先级。 main(); // 执行用户编写的代码、用户代码入口。 WRET; // 伪返回指令,实际是跳、JMP Thread_wait(); 和返回。 } Thread_wait(){ // 当前线程等待消息阻塞的方法。占用:4W;耗时:4ns A0.Process_XMUB.tblocked.PRR.L = 0; // 屏蔽当前线程。系统方法 // 编译成:R1 = A0; R1 = + Process_XMUB.blocked; R1.PRR.L = 0; // Process_XMUB.blocked是直接编译成对应于A0的相对行地址。所有的 // 进程的系统变量都如此。PRR(R6)是任务状态寄存器,低半字是当前线程号。 RET
}
Thread_wait()之后,线程处于阻塞等待状态,直到有相关消息到来;才会被唤醒。对于Thread_main(),可能运行一次后,就再也不能醒来。 Thread_start(线程对象名){ // 开始一个线程的方法,R0L等于线程号。 // R0L = 线程号; 线程号是编译器给出,占用:4W;耗时:4ns A0.Process_XMUB.tblocked.R0L = 1; // 打开屏蔽。 RET
}
但使用的写法通常是,线程对象名.start();编译器翻译后还是一样的。如果线程对象名到线程号的翻译是编译器进行的,即符号表中是一个线程对象名对应到一个线程对象号;就没问题。但如果线程对象名字是可以更改的,那就麻烦了;你从控制台更改,编译器是不知道的。问题是可以解决的,从对象头表中,一项一项搜索,直到匹配为止;也可以改用名字的哈希值来搜;但这,有没必要啊。或者,在进程中建立一个符号表;或者在系统中建一个注册表,晕吧。所以,APO程序中建立的对象名字、动态变量名字、类名字、方法名字、静态变量名字等等,在编译后、就不能更改!线程对象名字、线程对象数组成员、过程名字、信号名字、动态变量名字、类名字等等所映射的对象号,方法名字映射的相对地址,通通由编译器分配、建立。 1、线程调度 线程数最大64K个,或者说有64K个线程号;0号线程就是主线程了,每个进程中是必须有的。按优先级分为128组,每组最多512个线程;线程号越小,优先级就越高。我们构造一个64K位的位图变量Thread_WT;位顺序从0到最大64K,对应线程号;Thread_WT.0就是主线程了。如果用户编写的线程没指定优先级,编译器通常在2组或之后的组来分配;当然,你指定了;那会按你指定的来分配。线程号的分配就是资源小模式管理了,小模式属性表不单是线程号,还有静态对象号、动态变量号、事件过程号、类号等等。但这些,在APO中都是由编译器分配的。 对于Thread_WT的分配码,1是空闲、0是使用;对于屏蔽码tblocked,1是允许、0是禁止。所以,分配码 BIC 屏蔽码,即是分配码求反 AND 屏蔽码;表示的Thread_RUN_WT运行码,1是可运行、0是禁止。线程调度;是找到运行位图的第一个最低位得到的对应序号,再从对象头表中,据组序号找到相应的代码入口,goto。而这样,还是类似调用资源管理员啊。好吧,系统新方法:进程_对象_管理,ji_dx_gl()。消息过程和线程还是区别很大的,我们通常是把一些执行时间非常短的消息事件处理,放在消息过程中执行;全部的事件都处理完才交给到线程运行。消息过程有点类似中断的前半部,而线程就是中断的后半部了。 ji_dx_gl( 0.allot/release.0.位图行数, 1.no, A0.Process_XMUB, A0.位图变量 ){ // 分配,no为0;释放no是要给出你的序号(线程号)。要给出是那个组号位图 // 变量的。占用:22W。耗时:释放20ns,分配15ns + 位图行数 ns B2 = R0; B3 = R1; CMP.Z R1L, #0; // 如果是分配,跳。 JZ allot; R2 = + R1L>>8; // 只拷贝相对应的一行 YJMK.E = (R2).E; // 赋值R2指针指向的一行。 SS1; // 释放序号对应的1位。 (R2).E = YJMK.E; // 回传 RET allot: // 这段可能会与管理员方法GKLIYK()合并,只是差了(R3) = B4;。 COPY.E( YJMK, R3, R0L );// 编译后4条指令,耗时:3ns + R0L/2 ns SS1; // 分配。 BT0 PSR.YJ, UIBT; // 失败返回;不该回传。 R0L = B2L; COPY.E( R3, YJMK, R0L ); // 成功则回传。4W R0 = B3; // 返回结果 UIBT: RET } Thread_DD(){ // 线程调度方法。占用:14W。耗时:最大15*28 + 31 = 451ns // 失败、16*28 + 2 + 22550 = 23000 ns = 23us R4H = 16; // 最多16次循环 R3 = A0.Thread_RUN_WT; TDD: ji_dx_gl( 0.allot.0.16, 1.0 ); // 先拷贝16行,找到高优先级的线程位号;失败,下16行 BT1 PSR.YJ, IHGS; // 成功、跳。 R3 = +16; // 指针+16、继续。 R4H-; JNZ TDD; Thread_RUN_WT_BIC();// 没有可运行的线程,重新BIC下一个调度周期运行位图。 RET IHGS: R0L = >>9; // 得到线程组号。 R1 = A0.thread_lf_tab.R0L.W.W; // R1为对象头表对应线程类号的字地址。 JZ Thread_error; // 结果为0,为未安装跳错误处理。 PPC = (R1).W.W; // R1的内容作为线程入口基址送PPC,跳转到相应的线程 // 入口。线程执行后返回到进程代码块。 } 线程得到CPU后,其相应的运行位图中的位被ji_dx_gl()方法自动清除,允许别的线程有机会运行。当然,要成为自私线程;也可以清R0L的线程号最高位,在线程运行期间,A0.Thread_RUN_WT.R0L = 1; 嗯,非法使用系统管理的变量、编译器报错;线程在这区域读写,系统也会灭它!这时,除非有比你更高优先级的线程;否则,只有自私线程可以运行了。在任一个线程或过程的代码中,都可以动态生成一个新的线程或过程;也可以将自己,或他人挂起,A0.Process_XMUB.blocked.线程号 = 0; 屏蔽、只能调wait()方法实现、否则灭,不再参与调度。 一个线程可以从任何一个状态中调用wait() 方法 进入退出状态;线程一旦等待退出就不存在了,直到有消息唤醒。
Thread_RUN_WT_BIC(){ // BIC调度周期运行位图。占23W,耗时约:22.55us R2 = A0.Process_XMUB; R0L = 2K; R1 = R2 + 256; // 循环2K次, R1 = Process_XMUB.Thread_WT R0H = 0; R2 = + 512; // R2指向相应的屏蔽位图,Process_XMUB.tBlocked.E TRWB: R31 = (R2).W.W; R30 = (R1).W.W; // 循环BIC。 R4 = R31 BIC R30; JZ TRWB1; // 没有可运行位,跳。 R0H+; TRWB1:
(R3).W.W= R4; R1+; R2+; R3+; R0L-; JNZ TRWB; CMP R0H, #0; // 当进程的所有线程为退出状态时,进程会强行终止。 JZ TRWB2; R3L = R0H; Get_vnode_add(_Process, pid); //获得进程(文件)v节点指针的方法。 (R0).Thread_n.Z.Z = R3L; // 进程v节点的可运行线程数保存, RET
TRWB2:
PWRET; // 进程挂起,JMP Process_wait(); }
对象头表:以8位类号、对象号顺序排列的2W对象属性表、或方法表入口指针(块号、行开始地址,对象空间大小)构成的表。 类符号表:以类号顺序排列的类项构成的表;这是编译器程序构建、和使用的对象。每个类项占4行存储空间;2K个类项最多占用8KH。类项:前2行为类继承关系字数组,可标识32个父类以上的类号。第3行是子类变量,可标识16个下级子类号。对于具体类,第四行:16位一个参数,方法表长度,方法数,本类的对象数,属性表的属性项数,属性表的大小等等。应用程序声明的一个对象的属性表,编译器需要分析,该对象所属的类的继承关系;并正确的编译对象成员变量的偏移地址。比如,对象A、继承了B、C、D、E、F;而B、C、D又继承A1,E、F又继承A2。那么,编译器先是得到继承的关系类:B、A1、C、A1、D、A1、E、A2、F、A2,一个位图式排序就余下:B、A1、C、D、E、F、A2了。接着、编译器去掉那些继承声明、生成的对象格式应是:A、B、A1、C、D、E、F、A2。A排在最前面、其它类对象乱排就行了。你要写A.A1也行,A.B.A1、A.E.A2、A.A2等等都每关系,编译器都能给出正确的偏移地址;没有菱形问题。如果,程序员要直接操纵上万个对象;那凌乱得太难;从几十个根对象开始操纵,还行。所以,编译器在程序员的指点下;应对所有的对象进行打包处理。Di.对象名.最后的成员对象.变量,那就简单多了。把一大堆中间的…去掉,编译器应该能给出正确的变量偏移地址。那个C++的虚函数,我是半天不明白、莫名其妙;或许是C++的bug吧。本来,变量偏移地址的计算就是编译器的事情;跟用户程序没鸟毛关系。
2、线程的实现
1)、线程创建方法Thread_Create( 优先级组号priority_no, 动态对象行数 ); 编译器创建对象号,如果代码没设定优先级组号;那默认从第二组开始创建,如没空位置;则往后找。得到线程号、建立对象列表项;通常新线程是继承于一个线程类,其方法相对入口已知。跟着,如果是动态线程,申请动态对象的内存分配、动态对象号,创建一个线程类的对象;初始化。 设置线程优先级位图的相应位,打开屏蔽位start()方法;就投入到可运行状态;wait() 方法清0屏蔽位就进入停止状态。设计线程类必须包含run()方法,它是线程的入口。方法run的常规协定是,它可能执行任何所需的操作。
线程类的属性表通常有:线程入口、线程所属类号、大小,优先级组号、线程状态1W,线程定义的系统管理变量等。 用户创建一个新线程、并投入可运行状态;很简单,那就是在代码块中写下面一行: 线程类名 线程名字 = new( 优先级组号priority_no, 动态对象行数 ); 动态对象行数为0,则由编译器创建线程对象属性表。 编译器编译到这一行时,先据优先级组号分配线程号、得到最终优先级组号,而类号是已知的;并设置组线程优先级位图的相应位,那么编译成: Thread_Create(){ // 占用:8W;耗时:ns R31H = 类号; R31L = 优先级组号; R30H = 动态对象行数; R30L = 线程号; CMP.Z R30H, 0; // 编译器创建线程对象属性表、跳。 JZ TC1; mem_newH( R30H ); // 动态分配连续R30H行内存,返回R0L为动态对象号。 TC1: 相应线程类.init(); //初始化线程对象。
R1L = R30L; A0.Process_XMUB.blocked.R1L = 1; // 打开屏蔽、投入运行 RET } // 返回结果 R31、R30、R0。 2)、线程的返回 使用5种返回伪指令: WRET 对应 JMP Thread_wait(); 等待消息、阻塞返回 SRET 对应 JMP Thread_sleep( T ); 睡眠等待、阻塞返回 PRET 对应 JMP Thread_pend(); 分支定点等待消息、阻塞返回。唤醒后,可以回到代码分支的定点处;而非回到Run(); 比如一个分支某点处,open一个文件;那得到完成消息后,当然希望能回到该点、继续执行。 YRET 对应 JMP Thread_yield();线程让步返回,也可能下一个周期又运行。 TRET 对应 JMP Thread_stop();线程终结返回。资源回收、线程撤销;不用似主线程那样的僵死线程。 使用伪指令,可以少打一些E文。这些方法都是编起来很少的代码的、很简单的。在伪指令返回前,我们也可以启动另一个线程对象;如:线程对象名字.start(); 其它JAVA的方法好像就没多少用了。APO中的唤醒基本是基于信号、消息机制的;同步问题在APO中是很简单的,首先线程获得CPU时,就可不被打断的工作10ms;如果,你觉得代码运行时间有险,那你就先Key_thread();锁住线程调度吧;记得完成共享资源操作后解锁;Clr_key_thread()。 估计完整编写进程、线程类的方法代码能到达35E、值得庆贺。一点小玩意,就搞得这样长;还不知有多少错误。
三、信号、消息、事件过程 1、消息格式 APO中的消息格式,只有一种;信号也看作是一种消息。消息类型有系统消息、非系统消息,MSGV最高位定;又分为立即处理型、和线程处理型,MSGV次高位定。剩下的6位可表示64种消息类型,每种消息类型只有最大256个消息值。消息格式中,目标地址、和源地址占2W;消息内容只是2W。目前,系统消息类型只有一种:信号;用户消息类型只有3种:命令消息、窗口消息、通知消息、文件消息。回复消息时,可调用使消息头反转的发送消息方法。除了消息头格式不变外、消息内容是跟消息类型有很大关系的。 例子1:通常的客户/服务型消息格式。 BU1E MSG { // 请求消息结构。
BU16 fu_cpu_id; // 服务方CPU号,一个系统内最大支持64K个CPU。 BU16 fu_process_id; // 低13位是最大8K个服务方的进程号。 BU16 fu_thread_id; // 低16位是服务方的进程的线程号。 BU16 ku_cpu_id; // 客户方CPU号。 BU16 ku_process_id; // 低13位是最大8K个客户方的进程号。 BU16 ku_thread_id; // 低16位是客户方的进程的线程号。 BU16 MSGV; // 低8位消息值与高8位消息类型。 BU16 dx_id; // 目标或提交的对象号。 BU16 dx_add; // 对象的相对偏移地址。 BU16 dx_len; // 对象的大小。 BU64 TXC3; // 64位的附加信息,或是消息发送时鼠标所在的位置与信息。 BU32 time; // 消息发送的时间,单位ns。
} BU1E MSG { // 响应消息结构。 BU16 ku_cpu_id; // 客户方CPU号,一个系统内最大支持64K个CPU。 BU16 ku_process_id; // 低13位是最大8K个客户方的进程号。 BU16 ku_thread_id; // 低16位是客户方的进程的线程号。 BU16 fu_cpu_id; // 服务方CPU号。 BU16 fu_process_id; // 低13位是最大8K个服务方的进程号。 BU16 fu_thread_id; // 低16位是服务方的进程的线程号。 BU16 MSGV; // 低8位消息值与高8位消息类型。 BU16 dx_id; // 目标或提交的对象号。 BU16 dx_add; // 对象的相对偏移地址。 BU16 dx_len; // 对象的大小。 BU64 TXC3; // 64位的附加信息,或是消息发送时鼠标所在的位置与信息。 BU32 time; // 消息发送的时间,单位ns。
}
2、信号
待续。。。,
考虑到节省本地内存空间,需要对文件目录系统做大改动。。。。。