原文: Programming Notes

1. 移植

State Thread 库可移植到大多数类 UNIX 平台上,但是该库有几个部分需要依赖于平台特性,以下列出了这些部分:

  1. 线程上下文初始化。

    jmp_buf 数据结构的两个成员(程序计数器和堆栈指针)必须在创线程的时候进行手动设置。setjmp.h 文件中的

    jmp_buf 数据结构在不同平台有不同的定义。一般,程序计数器是名字为 PC 的结构体成员,堆栈指针是名字为 SP

    的结构体成员。可以参考 Netscape's NSPR library source 的源码。

    注意,在一些 BSD 派生的平台上,应该使用 setjmp(3)/longjmp(3) 替代 _setjmp(3)/_longjmp(3)(这调用仅操作堆栈和

    寄存器,不保存和恢复进程的信号掩码)。

    Linux 上的 glibc 库 从 2.4 开始,被 setjmp(3)/longjmp(3) 使用的 jmp_buf 数据结构不能被直接访问(除非在程序执行

    前设置特殊的环境变量 LD_POINTER_GUARD)。为了避免对特定环境的依赖,在所有的 Intel CPU 架构上 State

    Threads 库提供了setjmp/longjmp 的替代函数。由于 setjmp/longjmp 在许多 CPU 架构上都是可用的,因此在其他 CPU

    架构上也很容易被支持。

  2. 高精度的时间函数。

    一些平台(IRIX, Solaris)提供了一个基于硬件计数器的高精度时间函数。该函数返回过去某一时刻(通常是机器启动

    的时间)到现在的时间计数。它和一天的时刻无关,因此无法重置或清零。这对时间有高效、精准要求的任务十分有

    用。

    若一些特别的平台没有该函数,则可以使用 gettimeofday(3) 函数(尽管在某些平台上,该函数依赖于系统调用)。

  3. 堆栈的增长方向。

    该库需要确定堆栈的增长方向是朝下(还是朝上)或者是往高内存地址。我们可以写一个测试程序检测该平台的堆栈增

    长方向。

  4. 非阻塞特性继承。

    在一些平台(如 IRIX),由 accept 调用创建的 socket 套接字继承监听套接字的非阻塞特性。我们可以使用测试程序或

    用户手册来确认是否具有该特性。

  5. 匿名内存映射。

    State Threads 库在开辟线程堆栈内存时,使用匿名内存映射(mmap(2))。这个映射在 SVR4 和 BSD4.3 派生的平台

    上有所不同。通过使用 malloc(3) 来进行堆栈分配可以避免内存映射,在这种情况下,应该定义 MALLOC_STACK 宏。

所有与机器相关的特性测试宏都应该在 md.h 头文件中定义。所有 CPU 架构的关于 setjmp/longjmp 替代函数的汇编代码都应该

放在 md.S 文件中。

当前版本的库已支持以下平台的移植:

  • IRIX 6.x (both 32 and 64 bit)
  • Linux (kernel 2.x and glibc 2.x) on x86, Alpha, MIPS and MIPSEL, SPARC, ARM, PowerPC, 68k, HPPA, S390, IA-64,

    and Opteron (AMD-64)
  • Solaris 2.x (SunOS 5.x) on x86, AMD64, SPARC, and SPARC-64
  • AIX 4.x
  • HP-UX 11 (both 32 and 64 bit)
  • Tru64/OSF1
  • FreeBSD on x86, AMD64, and Alpha
  • OpenBSD on x86, AMD64, Alpha, and SPARC
  • NetBSD on x86, Alpha, SPARC, and VAX
  • MacOS X (Darwin/Tiger) on PowerPC and 32-bit and 64-bit x86
  • Cygwin

2. 信号

使用 State Threads 的应用程序中信号的处理应该与传统的 UNIX 进程应用程序相同。这里没有每个线程的信号掩码,所有线

程共享同一个信号处理函数,并且在信号处理函数中只能使用异步信号安全的函数。当然,可通过将信号事件转换为 I/O 事件

的方法来支持同步信号。下面的代码演示了这种技巧(为了清晰起见,忽略了错误处理):

/* Per-process pipe which is used as a signal queue. */
/* Up to PIPE_BUF/sizeof(int) signals can be queued up. */
int sig_pipe[2]; /* Signal catching function. */
/* Converts signal event to I/O event. */
void sig_catcher(int signo)
{
int err; /* Save errno to restore it after the write() */
err = errno;
/* write() is reentrant/async-safe */
write(sig_pipe[1], &signo, sizeof(int));
errno = err;
} /* Signal processing function. */
/* This is the "main" function of the signal processing thread. */
void *sig_process(void *arg)
{
st_netfd_t nfd;
int signo; nfd = st_netfd_open(sig_pipe[0]); for ( ;; ) {
/* Read the next signal from the pipe */
st_read(nfd, &signo, sizeof(int), ST_UTIME_NO_TIMEOUT); /* Process signal synchronously */
switch (signo) {
case SIGHUP:
/* do something here - reread config files, etc. */
break;
case SIGTERM:
/* do something here - cleanup, etc. */
break;
/**
* Other signals
*/
}
} return NULL;
} int main(int argc, char *argv[])
{
struct sigaction sa;
... /* Create signal pipe */
pipe(sig_pipe); /* Create signal processing thread */
st_thread_create(sig_process, NULL, 0, 0); /* Install sig_catcher() as a signal handler */
sa.sa_handler = sig_catcher;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sigaction(SIGHUP, &sa, NULL); sa.sa_handler = sig_catcher;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sigaction(SIGTERM, &sa, NULL);
... }

注意,如果使用了多个进程(看下面),信号管道应该在调用 fork(2) 之后进行初始化,以便每个进程都可以有自己的私有

管道。

3. 进程内同步

由于库 scheduler 的事件驱动特性,线程上下文切换(进程状态改变)只能在一组众所周知的库函数中发生。这个集合包含

一个线程可以阻塞的函数:I/O 函数(st_read(), st_write(), etc),sleep 函数(st_sleep(),etc),还有线程同步

函数(st_thread_join(), st_conf_wait(),etc)。因此,特定于进程的全局数据不需要被锁保护,因为线程不会在关键的部

分被重新调度(同一时刻只有一个线程可以访问相同的内存位置)。出于同样地原因,非线程安全的函数(在传统意义上)

可以被 State Threads 安全地使用。对于一个正确编写的应用程序(在关键部分没有阻塞函数),库的 mutex 实际上是没有

作用的,主要是为了提供完整性。锁的缺失极大的简化了应用程序的设计,并未可伸缩性提供了基础。

4. 进程间同步

State Threads 库可以将大量的并发连接转换成更少的单独进程,其中每个进程使用一个多对一的用户级线程实现(M:1 的 N

个映射而不是在某些平台上本地线程库使用的一个 M:N 的映射)。这种设计是应用程序可伸缩性的关键。可以这样想,好像

一组所有的线程都被划分为单独的组(进程),其中每个组都有一个单独的资源池(虚拟地址空间、文件描述符等)。应用程

序设计者完全控制应用程序创建的组(进程)的数量,以及通过标准 UNIX 进程间通信(IPC)在不同组间共享的资源。

创建多个进程有以下几个原因:

  • 方便利用系统中可用的多个硬件实体(CPU、磁盘等)(硬件并行性)。
  • 为了减少当一个进程崩溃时丢失大量的用户连接的风险。例如,如果 C 用户连接(线程)被复用到 P 进程中,其中一个进程

    崩溃了,那么所有的连接仅有一小部分(C/P)将丢失。
  • 为了克服操作系统对每个进程资源的限制。例如,如果 select(2) 用于事件轮询,每个进程并发连接(线程)的数量受到

    FD_SETSIZE 参数的限制(参见 select(2))。如果 FD_SETSIZE 等于 1024,每个连接需要一个文件描述符,那么应用

    程序应该创建 10 个进程来支持 10,000 个连接。

理想情况下,所有的用户会话都是完全独立的,因此不需要进程间通信。最好有几个独立的更小的特定于进程的资源(例如,

数据缓存),而不是所有进程共享一个大的资源。然而,有时需要在不同的进程间共享一个公共的资源。在这种情况下,可以

使用标准的 UNIX IPC。除此之外,还有一种方法可以同步不同的进程,这样只有当资源不可用时,访问共享资源的线程才会

被暂停(而不是整个进程)。在以下代码片段中,一个管道用作进程间同步的计数信号量:

#ifndef PIPE_BUF
#define PIPE_BUF 512 /* POSIX */
#endif /* Semaphore data structure */
typedef struct ipc_sem {
st_netfd_t rdfd; /* read descriptor */
st_netfd_t wrfd; /* write descriptor */
}ipc_sem_t; /* Create and initialize the semaphore. Should be called before fork(2). */
/* 'value' must be less than PIPE_BUF. */
/* If 'value' is 1, the semaphore works as mutex. */
ipc_sem_t *ipc_sem_create(int value)
{
ipc_sem_t *sem;
int p[2];
char b[PIPE_BUF]; /* Error checking is omitted for clarity */
sem = malloc(sizeof(ipc_sem_t)); /* Create the pipe */
pipe(p);
sem->rdfd = st_netfd_open(p[0]);
sem->wrfd = st_netfd_open(p[1]); /* Initalize the semaphore: put 'value' bytes into the pipe */
write(p[1], b, value); return sem;
} /* Try to decrement the "value" of the semaphore. */
/* If "value" is 0, the calling thread blocks on the semaphore. */
int ipc_sem_wait(ipc_sem_t *sem)
{
char c; /* Read one byte from the pipe */
if (st_read(sem->rdfd, &c, 1, ST_UTIME_NO_TIMEOUT) != 1)
return -1; return 0;
} /* Increment the "value" of the semaphore */
int ipc_sem_post(ipc_sem_t *sem)
{
char c; if (st_write(sem->wrfd, &c, 1, ST_UTIME_NO_TIMEOUT) != 1)
return -1; return 0;
}

通常,使用 State Threads 库编写应用程序的时候应该遵循以下步骤:

  1. 初始化库(st_init())。
  2. 创建将要在不同进程间共享的资源:创建和绑定侦听套接字,创建共享内存段,IPC 通道,同步原语等。
  3. 在每个子进程中创建一个线程池(st_thread_create())来处理用户连接。

5. 非网络 I/O

State Threads 体系结构使用 st_netfd_t 对象上的非阻塞 I/O 来并发处理多个用户连接。这种体系结构有一个缺点:整个进程

及其所有线程可能会在磁盘或其他非网络 I/O 操作期间阻塞,无论是通过 State Threads I/O 函数,直接系统调用,或者标准

I/O 函数。(这主要适用于磁盘读;磁盘写通常是异步执行的 -- 数据先写入到 buffer 缓存,然后再被写入到磁盘。)辛运的

是,磁盘 I/O(不像网络 I/O)通常需要一个有限且可预测的时间,但对于特殊设备或用户输入设备(包括 stdin)可能不适

用。然而,这样的 I/O 减少了系统的吞吐量,并增加了响应时间。有几种方法可以设计一个应用程序来避免这种缺点:

  • 如前所述,创建几个相同的主进程(对称架构)。这将提高 CPU 利用率,从而提高系统的总体吞吐量。
  • 除了处理阻塞 I/O 操作的主进程(不对称体系结构)之外,还有创建多个 "helper" 进程。这个方法是由 Peter Druschel

    等人在一篇论文中提出的。在这个架构中,主进程通过 IPC 通道(pipe(2), socketpair(2))与 "helper" 进程通信。主进程

    指示 "helper" 去执行潜在的阻塞操作。一旦操作完成,"helper" 将通过 IPC 返回通知。

6. 超时

st_cond_timedwait() 和 I/O 函数的超时参数是 st_sleep() 和 st_usleep() 自上一次上下文切换(而不是自上一次函数调用开始后)的最大时间。

State Threads 的时间分辨率实际上是上下文切换之间的时间间隔。在某些情况下,这个时间间隔可能很大,例如,当单个线程

连续执行大量工作时。注意,一个稳定的、不间断的网络 I/O 流符合这个描述;只有在线程阻塞时才会发生上下文切换。

如果指定的 I/O 超时小于上下文切换之间的时间间隔,则该函数可能会在函数开始调用后经过该时间量(指上下文切换的时间

间隔)之前返回超时错误。例如,如果自上一次上下文切换后已经过去了 8 ms,超时时间为 10 ms 的 I/O 函数可能会导致一

次切换,则在该函数调用后的 2 ms 返回一个超时错误(在 Linux 上,select() 的超时时间是 select() 返回前经过的时间的

上限)。同样,如果已经过去了 12 ms,函数可能立即返回。在几乎所有的情况下,I/O 超时应仅用于检测断开的网络连接或者

防止对方长时间保持空闲连接。因此,对于大多数应用程序来说,实际的 I/O 超时应该在几秒钟内。此外,重试超时操作可能

没有意义,也不要重试使用一个简单的更大的超时。

最大的有效超时值取决于平台,并且可能会显著的小于 select() 的 INT_MAX 秒 或 poll() 的 INT_MAX 毫秒。一般情况下,

你不应该使用超过几个小时的超时。使用 ST_UTIME_NO_TIMEOUT(-1) 作为一个特殊值来指示无限超时或者无限期休眠。使

用 ST_UTIME_NO_WAIT(0) 来指示不等待。

State Threads之编程注意事项的更多相关文章

  1. State Threads 回调终结者

    上回写了篇<一个“蝇量级”C语言协程库>,推荐了一下Protothreads,通过coroutine模拟了用户级别的multi-threading模型,虽然本身足够“轻”,杜绝了系统开销, ...

  2. State Threads——异步回调的线性实现

    State Threads——异步回调的线性实现 原文链接:http://coolshell.cn/articles/12012.html 本文的标题看起来有点拗口,其实State Threads库就 ...

  3. state Threads 开源库介绍

    译文在后面. State Threads for Internet Applications Introduction State Threads is an application library ...

  4. 优秀开源项目之三:高性能、高并发、高扩展性和可读性的网络服务器架构State Threads

    译文在后面. State Threads for Internet Applications Introduction State Threads is an application library ...

  5. State Threads之网络架构库

    原文: State Threads for Internet Applications 介绍 State Threads is an application library which provide ...

  6. [百度空间] [原]跨平台编程注意事项(二): windows下 x86到x64的移植

    之前转的: 将程序移植到64位Windows 还有自己乱写的一篇: 跨平台编程注意事项(一) 之前对于x64平台的移植都是纸上谈兵,算是前期准备工作, 但起码在写代码时,已经非常注意了.所以现在移植起 ...

  7. 协程库st(state threads library)原理解析

    协程库state threads library(以下简称st)是一个基于setjmp/longjmp实现的C语言版用户线程库或协程库(user level thread). 这里有一个基本的协程例子 ...

  8. State Threads之co-routine的创建和stack的管理

    1. 综述 协程库 State Threads Library 是一个基于 setjmp/longjmp 实现的 C 语言版用户线程库或协程库(user level thread). 基本协程例子: ...

  9. [百度空间] [原]跨平台编程注意事项(三): window 到 android 的 移植

    大的问题 先记录一下跨平台时需要注意的大方向. 1.OS和CPU 同一个操作系统, CPU也可能是不一样的, 比如windows也有基于arm CPU的版本,而android目前有x86,arm,mi ...

随机推荐

  1. spring注解定时器

    上一篇文章写了一个在配置文件中设置时间的定时器,现在来写一个注解方式的定时器: 1.工程结构如下: 2.需要执行的代码块: package com.Task; import org.springfra ...

  2. python之时间日期datetime

    相比于time模块,datetime模块的接口则更直观.更容易调用datetime模块定义了以下几个类: datetime.date():表示日期的类.常用的属性是year,month,day:dat ...

  3. 用Python+Aria2写一个自动选择最优下载方式的E站爬虫

    前言 E站爬虫在网上已经有很多了,但多数都只能以图片为单位下载,且偶尔会遇到图片加载失败的情况:熟悉E站的朋友们应该知道,E站许多资源都是有提供BT种子的,而且通常打包的是比默认看图模式更高清的文件: ...

  4. IPC之ipc_sysctl.c源码解读

    // SPDX-License-Identifier: GPL-2.0-only /* * Copyright (C) 2007 * * Author: Eric Biederman <ebie ...

  5. 简单了解Linux文件目录

    /bin :获得最小的系统可操作性所需要的命令 /boot :内核和加载内核所需的文件 /dev :终端.磁盘.调制解调器等的设备项 /etc :关键的启动文件和配置文件 /home :用户的主目录 ...

  6. POJ 1741 单次询问树上距离<=K的点对数 点分治

    #include<cstdio> #include<cstring> #include<algorithm> using namespace std; ; ; ], ...

  7. codeforces 576C Points on Plane 相邻两点的欧拉距离

    题意:给出n个点,要求排序后,相邻两点的欧拉距离之和小于等于2.5e9做法:由于0≤ xi, yi ≤ 1e6,所以可以将x<=1000的点分成一份,1000<x<=2000的点分成 ...

  8. 数据库允许空值(null),往往是悲剧的开始(1分钟系列)

    数据库字段允许空值,会遇到一些问题,此处包含的一些知识点,和大家聊一聊. 数据准备: create table user ( id int, name varchar(20), index(id) ) ...

  9. Resource ResourceLoader

    DefaultResourceLoader   -- > ResourceLoader 方法 ResourceLoader getResource(String location); Class ...

  10. MariaDB安装与使用

    下载地址:https://downloads.mariadb.org/ 下载相对应的电脑版本程序 等待下载完成...... 安装教程: 双击运行 设置数据库的密码 等待安装完成.. 这样就完成安装了. ...