st.html 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504
  1. <HTML>
  2. <HEAD>
  3. <TITLE>State Threads for Internet Applications</TITLE>
  4. </HEAD>
  5. <BODY BGCOLOR=#FFFFFF>
  6. <H2>State Threads for Internet Applications</H2>
  7. <H3>Introduction</H3>
  8. <P>
  9. State Threads is an application library which provides a
  10. foundation for writing fast and highly scalable Internet Applications
  11. on UNIX-like platforms. It combines the simplicity of the multithreaded
  12. programming paradigm, in which one thread supports each simultaneous
  13. connection, with the performance and scalability of an event-driven
  14. state machine architecture.</P>
  15. <H3>1. Definitions</H3>
  16. <P>
  17. <A NAME="IA">
  18. <H4>1.1 Internet Applications</H4>
  19. </A>
  20. <P>
  21. An <I>Internet Application</I> (IA) is either a server or client network
  22. application that accepts connections from clients and may or may not
  23. connect to servers. In an IA the arrival or departure of network data
  24. often controls processing (that is, IA is a <I>data-driven</I> application).
  25. For each connection, an IA does some finite amount of work
  26. involving data exchange with its peer, where its peer may be either
  27. a client or a server.
  28. The typical transaction steps of an IA are to accept a connection,
  29. read a request, do some finite and predictable amount of work to
  30. process the request, then write a response to the peer that sent the
  31. request. One example of an IA is a Web server;
  32. the most general example of an IA is a proxy server, because it both
  33. accepts connections from clients and connects to other servers.</P>
  34. <P>
  35. We assume that the performance of an IA is constrained by available CPU
  36. cycles rather than network bandwidth or disk I/O (that is, CPU
  37. is a bottleneck resource).
  38. <P>
  39. <A NAME="PS">
  40. <H4>1.2 Performance and Scalability</H4>
  41. </A>
  42. <P>
  43. The <I>performance</I> of an IA is usually evaluated as its
  44. throughput measured in transactions per second or bytes per second (one
  45. can be converted to the other, given the average transaction size). There are
  46. several benchmarks that can be used to measure throughput of Web serving
  47. applications for specific workloads (such as
  48. <A HREF="http://www.spec.org/osg/web96/">SPECweb96</A>,
  49. <A HREF="http://www.mindcraft.com/webstone/">WebStone</A>,
  50. <A HREF="http://www.zdnet.com/zdbop/webbench/">WebBench</A>).
  51. Although there is no common definition for <I>scalability</I>, in general it
  52. expresses the ability of an application to sustain its performance when some
  53. external condition changes. For IAs this external condition is either the
  54. number of clients (also known as "users," "simultaneous connections," or "load
  55. generators") or the underlying hardware system size (number of CPUs, memory
  56. size, and so on). Thus there are two types of scalability: <I>load
  57. scalability</I> and <I>system scalability</I>, respectively.
  58. <P>
  59. The figure below shows how the throughput of an idealized IA changes with
  60. the increasing number of clients (solid blue line). Initially the throughput
  61. grows linearly (the slope represents the maximal throughput that one client
  62. can provide). Within this initial range, the IA is underutilized and CPUs are
  63. partially idle. Further increase in the number of clients leads to a system
  64. saturation, and the throughput gradually stops growing as all CPUs become fully
  65. utilized. After that point, the throughput stays flat because there are no
  66. more CPU cycles available.
  67. In the real world, however, each simultaneous connection
  68. consumes some computational and memory resources, even when idle, and this
  69. overhead grows with the number of clients. Therefore, the throughput of the
  70. real world IA starts dropping after some point (dashed blue line in the figure
  71. below). The rate at which the throughput drops depends, among other things, on
  72. application design.
  73. <P>
  74. We say that an application has a good <I>load scalability</I> if it can
  75. sustain its throughput over a wide range of loads.
  76. Interestingly, the <A HREF="http://www.spec.org/osg/web99/">SPECweb99</A>
  77. benchmark somewhat reflects the Web server's load scalability because it
  78. measures the number of clients (load generators) given a mandatory minimal
  79. throughput per client (that is, it measures the server's <I>capacity</I>).
  80. This is unlike <A HREF="http://www.spec.org/osg/web96/">SPECweb96</A> and
  81. other benchmarks that use the throughput as their main metric (see the figure
  82. below).
  83. <P>
  84. <CENTER><IMG SRC="fig.gif" ALT="Figure: Throughput vs. Number of clients">
  85. </CENTER>
  86. <P>
  87. <I>System scalability</I> is the ability of an application to sustain its
  88. performance per hardware unit (such as a CPU) with the increasing number of
  89. these units. In other words, good system scalability means that doubling the
  90. number of processors will roughly double the application's throughput (dashed
  91. green line). We assume here that the underlying operating system also scales
  92. well. Good system scalability allows you to initially run an application on
  93. the smallest system possible, while retaining the ability to move that
  94. application to a larger system if necessary, without excessive effort or
  95. expense. That is, an application need not be rewritten or even undergo a
  96. major porting effort when changing system size.
  97. <P>
  98. Although scalability and performance are more important in the case of server
  99. IAs, they should also be considered for some client applications (such as
  100. benchmark load generators).
  101. <P>
  102. <A NAME="CONC">
  103. <H4>1.3 Concurrency</H4>
  104. </A>
  105. <P>
  106. Concurrency reflects the parallelism in a system. The two unrelated types
  107. are <I>virtual</I> concurrency and <I>real</I> concurrency.
  108. <UL>
  109. <LI>Virtual (or apparent) concurrency is the number of simultaneous
  110. connections that a system supports.
  111. <BR><BR>
  112. <LI>Real concurrency is the number of hardware devices, including
  113. CPUs, network cards, and disks, that actually allow a system to perform
  114. tasks in parallel.
  115. </UL>
  116. <P>
  117. An IA must provide virtual concurrency in order to serve many users
  118. simultaneously.
  119. To achieve maximum performance and scalability in doing so, the number of
  120. programming entities than an IA creates to be scheduled by the OS kernel
  121. should be
  122. kept close to (within an order of magnitude of) the real concurrency found on
  123. the system. These programming entities scheduled by the kernel are known as
  124. <I>kernel execution vehicles</I>. Examples of kernel execution vehicles
  125. include Solaris lightweight processes and IRIX kernel threads.
  126. In other words, the number of kernel execution vehicles should be dictated by
  127. the system size and not by the number of simultaneous connections.
  128. <P>
  129. <H3>2. Existing Architectures</H3>
  130. <P>
  131. There are a few different architectures that are commonly used by IAs.
  132. These include the <I>Multi-Process</I>,
  133. <I>Multi-Threaded</I>, and <I>Event-Driven State Machine</I>
  134. architectures.
  135. <P>
  136. <A NAME="MP">
  137. <H4>2.1 Multi-Process Architecture</H4>
  138. </A>
  139. <P>
  140. In the Multi-Process (MP) architecture, an individual process is
  141. dedicated to each simultaneous connection.
  142. A process performs all of a transaction's initialization steps
  143. and services a connection completely before moving on to service
  144. a new connection.
  145. <P>
  146. User sessions in IAs are relatively independent; therefore, no
  147. synchronization between processes handling different connections is
  148. necessary. Because each process has its own private address space,
  149. this architecture is very robust. If a process serving one of the connections
  150. crashes, the other sessions will not be affected. However, to serve many
  151. concurrent connections, an equal number of processes must be employed.
  152. Because processes are kernel entities (and are in fact the heaviest ones),
  153. the number of kernel entities will be at least as large as the number of
  154. concurrent sessions. On most systems, good performance will not be achieved
  155. when more than a few hundred processes are created because of the high
  156. context-switching overhead. In other words, MP applications have poor load
  157. scalability.
  158. <P>
  159. On the other hand, MP applications have very good system scalability, because
  160. no resources are shared among different processes and there is no
  161. synchronization overhead.
  162. <P>
  163. The Apache Web Server 1.x (<A HREF=#refs1>[Reference 1]</A>) uses the MP
  164. architecture on UNIX systems.
  165. <P>
  166. <A NAME="MT">
  167. <H4>2.2 Multi-Threaded Architecture</H4>
  168. </A>
  169. <P>
  170. In the Multi-Threaded (MT) architecture, multiple independent threads
  171. of control are employed within a single shared address space. Like a
  172. process in the MP architecture, each thread performs all of a
  173. transaction's initialization steps and services a connection completely
  174. before moving on to service a new connection.
  175. <P>
  176. Many modern UNIX operating systems implement a <I>many-to-few</I> model when
  177. mapping user-level threads to kernel entities. In this model, an
  178. arbitrarily large number of user-level threads is multiplexed onto a
  179. lesser number of kernel execution vehicles. Kernel execution
  180. vehicles are also known as <I>virtual processors</I>. Whenever a user-level
  181. thread makes a blocking system call, the kernel execution vehicle it is using
  182. will become blocked in the kernel. If there are no other non-blocked kernel
  183. execution vehicles and there are other runnable user-level threads, a new
  184. kernel execution vehicle will be created automatically. This prevents the
  185. application from blocking when it can continue to make useful forward
  186. progress.
  187. <P>
  188. Because IAs are by nature network I/O driven, all concurrent sessions block on
  189. network I/O at various points. As a result, the number of virtual processors
  190. created in the kernel grows close to the number of user-level threads
  191. (or simultaneous connections). When this occurs, the many-to-few model
  192. effectively degenerates to a <I>one-to-one</I> model. Again, like in
  193. the MP architecture, the number of kernel execution vehicles is dictated by
  194. the number of simultaneous connections rather than by number of CPUs. This
  195. reduces an application's load scalability. However, because kernel threads
  196. (lightweight processes) use fewer resources and are more light-weight than
  197. traditional UNIX processes, an MT application should scale better with load
  198. than an MP application.
  199. <P>
  200. Unexpectedly, the small number of virtual processors sharing the same address
  201. space in the MT architecture destroys an application's system scalability
  202. because of contention among the threads on various locks. Even if an
  203. application itself is carefully
  204. optimized to avoid lock contention around its own global data (a non-trivial
  205. task), there are still standard library functions and system calls
  206. that use common resources hidden from the application. For example,
  207. on many platforms thread safety of memory allocation routines
  208. (<TT>malloc(3)</TT>, <TT>free(3)</TT>, and so on) is achieved by using a single
  209. global lock. Another example is a per-process file descriptor table.
  210. This common resource table is shared by all kernel execution vehicles within
  211. the same process and must be protected when one modifies it via
  212. certain system calls (such as <TT>open(2)</TT>, <TT>close(2)</TT>, and so on).
  213. In addition to that, maintaining the caches coherent
  214. among CPUs on multiprocessor systems hurts performance when different threads
  215. running on different CPUs modify data items on the same cache line.
  216. <P>
  217. In order to improve load scalability, some applications employ a different
  218. type of MT architecture: they create one or more thread(s) <I>per task</I>
  219. rather than one thread <I>per connection</I>. For example, one small group
  220. of threads may be responsible for accepting client connections, another
  221. for request processing, and yet another for serving responses. The main
  222. advantage of this architecture is that it eliminates the tight coupling
  223. between the number of threads and number of simultaneous connections. However,
  224. in this architecture, different task-specific thread groups must share common
  225. work queues that must be protected by mutual exclusion locks (a typical
  226. producer-consumer problem). This adds synchronization overhead that causes an
  227. application to perform badly on multiprocessor systems. In other words, in
  228. this architecture, the application's system scalability is sacrificed for the
  229. sake of load scalability.
  230. <P>
  231. Of course, the usual nightmares of threaded programming, including data
  232. corruption, deadlocks, and race conditions, also make MT architecture (in any
  233. form) non-simplistic to use.
  234. <P>
  235. <A NAME="EDSM">
  236. <H4>2.3 Event-Driven State Machine Architecture</H4>
  237. </A>
  238. <P>
  239. In the Event-Driven State Machine (EDSM) architecture, a single process
  240. is employed to concurrently process multiple connections. The basics of this
  241. architecture are described in Comer and Stevens
  242. <A HREF=#refs2>[Reference 2]</A>.
  243. The EDSM architecture performs one basic data-driven step associated with
  244. a particular connection at a time, thus multiplexing many concurrent
  245. connections. The process operates as a state machine that receives an event
  246. and then reacts to it.
  247. <P>
  248. In the idle state the EDSM calls <TT>select(2)</TT> or <TT>poll(2)</TT> to
  249. wait for network I/O events. When a particular file descriptor is ready for
  250. I/O, the EDSM completes the corresponding basic step (usually by invoking a
  251. handler function) and starts the next one. This architecture uses
  252. non-blocking system calls to perform asynchronous network I/O operations.
  253. For more details on non-blocking I/O see Stevens
  254. <A HREF=#refs3>[Reference 3]</A>.
  255. <P>
  256. To take advantage of hardware parallelism (real concurrency), multiple
  257. identical processes may be created. This is called Symmetric Multi-Process
  258. EDSM and is used, for example, in the Zeus Web Server
  259. (<A HREF=#refs4>[Reference 4]</A>). To more efficiently multiplex disk I/O,
  260. special "helper" processes may be created. This is called Asymmetric
  261. Multi-Process EDSM and was proposed for Web servers by Druschel
  262. and others <A HREF=#refs5>[Reference 5]</A>.
  263. <P>
  264. EDSM is probably the most scalable architecture for IAs.
  265. Because the number of simultaneous connections (virtual concurrency) is
  266. completely decoupled from the number of kernel execution vehicles (processes),
  267. this architecture has very good load scalability. It requires only minimal
  268. user-level resources to create and maintain additional connection.
  269. <P>
  270. Like MP applications, Multi-Process EDSM has very good system scalability
  271. because no resources are shared among different processes and there is no
  272. synchronization overhead.
  273. <P>
  274. Unfortunately, the EDSM architecture is monolithic rather than based on the
  275. concept of threads, so new applications generally need to be implemented from
  276. the ground up. In effect, the EDSM architecture simulates threads and their
  277. stacks the hard way.
  278. <P>
  279. <A NAME="ST">
  280. <H3>3. State Threads Library</H3>
  281. </A>
  282. <P>
  283. The State Threads library combines the advantages of all of the above
  284. architectures. The interface preserves the programming simplicity of thread
  285. abstraction, allowing each simultaneous connection to be treated as a separate
  286. thread of execution within a single process. The underlying implementation is
  287. close to the EDSM architecture as the state of each particular concurrent
  288. session is saved in a separate memory segment.
  289. <P>
  290. <H4>3.1 State Changes and Scheduling</H4>
  291. <P>
  292. The state of each concurrent session includes its stack environment
  293. (stack pointer, program counter, CPU registers) and its stack. Conceptually,
  294. a thread context switch can be viewed as a process changing its state. There
  295. are no kernel entities involved other than processes.
  296. Unlike other general-purpose threading libraries, the State Threads library
  297. is fully deterministic. The thread context switch (process state change) can
  298. only happen in a well-known set of functions (at I/O points or at explicit
  299. synchronization points). As a result, process-specific global data does not
  300. have to be protected by mutual exclusion locks in most cases. The entire
  301. application is free to use all the static variables and non-reentrant library
  302. functions it wants, greatly simplifying programming and debugging while
  303. increasing performance. This is somewhat similar to a <I>co-routine</I> model
  304. (co-operatively multitasked threads), except that no explicit yield is needed
  305. --
  306. sooner or later, a thread performs a blocking I/O operation and thus surrenders
  307. control. All threads of execution (simultaneous connections) have the
  308. same priority, so scheduling is non-preemptive, like in the EDSM architecture.
  309. Because IAs are data-driven (processing is limited by the size of network
  310. buffers and data arrival rates), scheduling is non-time-slicing.
  311. <P>
  312. Only two types of external events are handled by the library's
  313. scheduler, because only these events can be detected by
  314. <TT>select(2)</TT> or <TT>poll(2)</TT>: I/O events (a file descriptor is ready
  315. for I/O) and time events
  316. (some timeout has expired). However, other types of events (such as
  317. a signal sent to a process) can also be handled by converting them to I/O
  318. events. For example, a signal handling function can perform a write to a pipe
  319. (<TT>write(2)</TT> is reentrant/asynchronous-safe), thus converting a signal
  320. event to an I/O event.
  321. <P>
  322. To take advantage of hardware parallelism, as in the EDSM architecture,
  323. multiple processes can be created in either a symmetric or asymmetric manner.
  324. Process management is not in the library's scope but instead is left up to the
  325. application.
  326. <P>
  327. There are several general-purpose threading libraries that implement a
  328. <I>many-to-one</I> model (many user-level threads to one kernel execution
  329. vehicle), using the same basic techniques as the State Threads library
  330. (non-blocking I/O, event-driven scheduler, and so on). For an example, see GNU
  331. Portable Threads (<A HREF=#refs6>[Reference 6]</A>). Because they are
  332. general-purpose, these libraries have different objectives than the State
  333. Threads library. The State Threads library is <I>not</I> a general-purpose
  334. threading library,
  335. but rather an application library that targets only certain types of
  336. applications (IAs) in order to achieve the highest possible performance and
  337. scalability for those applications.
  338. <P>
  339. <H4>3.2 Scalability</H4>
  340. <P>
  341. State threads are very lightweight user-level entities, and therefore creating
  342. and maintaining user connections requires minimal resources. An application
  343. using the State Threads library scales very well with the increasing number
  344. of connections.
  345. <P>
  346. On multiprocessor systems an application should create multiple processes
  347. to take advantage of hardware parallelism. Using multiple separate processes
  348. is the <I>only</I> way to achieve the highest possible system scalability.
  349. This is because duplicating per-process resources is the only way to avoid
  350. significant synchronization overhead on multiprocessor systems. Creating
  351. separate UNIX processes naturally offers resource duplication. Again,
  352. as in the EDSM architecture, there is no connection between the number of
  353. simultaneous connections (which may be very large and changes within a wide
  354. range) and the number of kernel entities (which is usually small and constant).
  355. In other words, the State Threads library makes it possible to multiplex a
  356. large number of simultaneous connections onto a much smaller number of
  357. separate processes, thus allowing an application to scale well with both
  358. the load and system size.
  359. <P>
  360. <H4>3.3 Performance</H4>
  361. <P>
  362. Performance is one of the library's main objectives. The State Threads
  363. library is implemented to minimize the number of system calls and
  364. to make thread creation and context switching as fast as possible.
  365. For example, per-thread signal mask does not exist (unlike
  366. POSIX threads), so there is no need to save and restore a process's
  367. signal mask on every thread context switch. This eliminates two system
  368. calls per context switch. Signal events can be handled much more
  369. efficiently by converting them to I/O events (see above).
  370. <P>
  371. <H4>3.4 Portability</H4>
  372. <P>
  373. The library uses the same general, underlying concepts as the EDSM
  374. architecture, including non-blocking I/O, file descriptors, and
  375. I/O multiplexing. These concepts are available in some form on most
  376. UNIX platforms, making the library very portable across many
  377. flavors of UNIX. There are only a few platform-dependent sections in the
  378. source.
  379. <P>
  380. <H4>3.5 State Threads and NSPR</H4>
  381. <P>
  382. The State Threads library is a derivative of the Netscape Portable
  383. Runtime library (NSPR) <A HREF=#refs7>[Reference 7]</A>. The primary goal of
  384. NSPR is to provide a platform-independent layer for system facilities,
  385. where system facilities include threads, thread synchronization, and I/O.
  386. Performance and scalability are not the main concern of NSPR. The
  387. State Threads library addresses performance and scalability while
  388. remaining much smaller than NSPR. It is contained in 8 source files
  389. as opposed to more than 400, but provides all the functionality that
  390. is needed to write efficient IAs on UNIX-like platforms.
  391. <P>
  392. <TABLE CELLPADDING=3>
  393. <TR>
  394. <TD></TD>
  395. <TH>NSPR</TH>
  396. <TH>State Threads</TH>
  397. </TR>
  398. <TR>
  399. <TD><B>Lines of code</B></TD>
  400. <TD ALIGN=RIGHT>~150,000</TD>
  401. <TD ALIGN=RIGHT>~3000</TD>
  402. </TR>
  403. <TR>
  404. <TD><B>Dynamic library size&nbsp;&nbsp;<BR>(debug version)</B></TD>
  405. <TD></TD>
  406. <TD></TD>
  407. </TR>
  408. <TR>
  409. <TD>IRIX</TD>
  410. <TD ALIGN=RIGHT>~700 KB</TD>
  411. <TD ALIGN=RIGHT>~60 KB</TD>
  412. </TR>
  413. <TR>
  414. <TD>Linux</TD>
  415. <TD ALIGN=RIGHT>~900 KB</TD>
  416. <TD ALIGN=RIGHT>~70 KB</TD>
  417. </TR>
  418. </TABLE>
  419. <P>
  420. <H3>Conclusion</H3>
  421. <P>
  422. State Threads is an application library which provides a foundation for
  423. writing <A HREF=#IA>Internet Applications</A>. To summarize, it has the
  424. following <I>advantages</I>:
  425. <P>
  426. <UL>
  427. <LI>It allows the design of fast and highly scalable applications. An
  428. application will scale well with both load and number of CPUs.
  429. <P>
  430. <LI>It greatly simplifies application programming and debugging because, as a
  431. rule, no mutual exclusion locking is necessary and the entire application is
  432. free to use static variables and non-reentrant library functions.
  433. </UL>
  434. <P>
  435. The library's main <I>limitation</I>:
  436. <P>
  437. <UL>
  438. <LI>All I/O operations on sockets must use the State Thread library's I/O
  439. functions because only those functions perform thread scheduling and prevent
  440. the application's processes from blocking.
  441. </UL>
  442. <P>
  443. <H3>References</H3>
  444. <OL>
  445. <A NAME="refs1">
  446. <LI> Apache Software Foundation,
  447. <A HREF="http://www.apache.org">http://www.apache.org</A>.
  448. <A NAME="refs2">
  449. <LI> Douglas E. Comer, David L. Stevens, <I>Internetworking With TCP/IP,
  450. Vol. III: Client-Server Programming And Applications</I>, Second Edition,
  451. Ch. 8, 12.
  452. <A NAME="refs3">
  453. <LI> W. Richard Stevens, <I>UNIX Network Programming</I>, Second Edition,
  454. Vol. 1, Ch. 15.
  455. <A NAME="refs4">
  456. <LI> Zeus Technology Limited,
  457. <A HREF="http://www.zeus.co.uk/">http://www.zeus.co.uk</A>.
  458. <A NAME="refs5">
  459. <LI> Peter Druschel, Vivek S. Pai, Willy Zwaenepoel,
  460. <A HREF="http://www.cs.rice.edu/~druschel/usenix99flash.ps.gz">
  461. Flash: An Efficient and Portable Web Server</A>. In <I>Proceedings of the
  462. USENIX 1999 Annual Technical Conference</I>, Monterey, CA, June 1999.
  463. <A NAME="refs6">
  464. <LI> GNU Portable Threads,
  465. <A HREF="http://www.gnu.org/software/pth/">http://www.gnu.org/software/pth/</A>.
  466. <A NAME="refs7">
  467. <LI> Netscape Portable Runtime,
  468. <A HREF="http://www.mozilla.org/docs/refList/refNSPR/">http://www.mozilla.org/docs/refList/refNSPR/</A>.
  469. </OL>
  470. <H3>Other resources covering various architectural issues in IAs</H3>
  471. <OL START=8>
  472. <LI> Dan Kegel, <I>The C10K problem</I>,
  473. <A HREF="http://www.kegel.com/c10k.html">http://www.kegel.com/c10k.html</A>.
  474. </LI>
  475. <LI> James C. Hu, Douglas C. Schmidt, Irfan Pyarali, <I>JAWS: Understanding
  476. High Performance Web Systems</I>,
  477. <A HREF="http://www.cs.wustl.edu/~jxh/research/research.html">http://www.cs.wustl.edu/~jxh/research/research.html</A>.</LI>
  478. </OL>
  479. <P>
  480. <HR>
  481. <P>
  482. <CENTER><FONT SIZE=-1>Portions created by SGI are Copyright &copy; 2000
  483. Silicon Graphics, Inc. All rights reserved.</FONT></CENTER>
  484. <P>
  485. </BODY>
  486. </HTML>