对于多进程的webserver而言需要注意两个问题:
父子进程需要及时关闭自己不需要用的fd(因为在fork之后connfd交给child处理,所以parent需要关闭自己的,而对于child而言在fork时获取的listenfd无用,也要先关闭,特别时parent的及时关闭,否则会造成泄露)
在parent进行reap的时候需要考虑到有多个child需要reap(可参见CSAPP(8)Exception Control Flow里的currency一节)
#include
#include
//return nonzero count of ready descriptors,-1 on error
int select(int n,fd_set *fdset,NULL,NULL,NULL);
FD_ZERO(fd_set *fdset);//clear all bits
FD_CLR(int fd,fd_set *fdset);//clear bit fd
FD_SET(int fd,fd_set *fdset);//turn on bit fd
FD_ISSET(int fd,fd_set *fdset);//is bit fd on?
一般而言, n = m a x ( f d ) + 1 n=max(fd)+1 n=max(fd)+1,而fdset在每次调用select时都会被select改变,然后调用方通过FD_ISSET来判断是哪个fd从而处理。在调用方处理完成后重新设置fdset然后再次调用select
一种把IO multiplexing用作event-driven的视角是把逻辑处理视为state machine,也就是一些 ( I n p u t E v e n t , I n p u t S t a t e ) → O u t p u t S t a t e (InputEvent ,InputState)\rightarrow OutputState (InputEvent,InputState)→OutputState,特殊的,有一种self-loop里InputState和OutputState相同
下面代码展示了state machine的结构,由select来获得新的event,然后由check_clients做出状态转移
#include
#include
#include
#include
#include
#include
#define RIO_BUFSIZE 8192
typedef struct {
int rio_fd;
int rio_cnt;
char *rio_bufptr;
char rio_buf[RIO_BUFSIZE];
} rio_t;
typedef struct {
int maxfd;
fd_set read_set;
fd_set ready_set;
int nready; //为了让循环中快速结束而无须遍历整个数组
int maxi;
int clientfd[FD_SETSIZE];
rio_t clientrio[FD_SETSIZE]
}pool;
int byte_cnt=0;
int open_listenfd(int port);
void rio_readinitb(rio_t *ptr, int connfd);
void init_pool(int listenfd, pool *p){
int i;
p->maxi=-1;
for(i=0;i<FD_SETSIZE;i++){
p->clientfd[i]=-1;
}
p->maxfd=listenfd;
FD_ZERO(&p->read_set);
FD_SET(listenfd,&p->read_set);
}
void add_client(int connfd,pool *p){
int i;
p->nready--;
for(i=0;i<FD_SETSIZE;i++){
if(p->clientfd[i]<0){
p->clientfd[i]=connfd;
rio_readinitb(&p->clientrio[i],connfd);
FD_SET(connfd,&p->read_set);
if(connfd>p->maxfd){
p->maxfd=connfd;
}
if(i>p->maxi){
p->maxi=i;
}
break;
}
}
if(i==FD_SETSIZE){
app_error("add client error:Too Many Clients");
}
}
void rio_readinitb(rio_t *ptr, int connfd) {
ptr->rio_fd=connfd;
ptr->rio_cnt=0;
ptr->rio_bufptr=ptr->rio_buf;
}
static ssize_t rio_read(rio_t *rp,char *usrbuf,size_t n){
int cnt;
while (rp->rio_cnt<=0){//refill if buf is empty
rp->rio_cnt=read(rp->rio_fd,rp->rio_buf,sizeof(rp->rio_buf));
if(rp->rio_cnt<0){
if (EINTR!=errno){
return -1;
} else if(0==rp->rio_cnt){
return 0;//EOF
} else{
rp->rio_bufptr=rp->rio_buf;//reset
}
}
}
cnt=n;
if(rp->rio_cnt<n){
cnt=rp->rio_cnt;
}
memcpy(usrbuf,rp->rio_bufptr,cnt);
rp->rio_bufptr+=cnt;
rp->rio_cnt-=cnt;
return cnt;
}
ssize_t rio_readlineb(rio_t *rp,void *usrbuf,size_t maxlen){
int n,rc;
char c,*bufp=usrbuf;
for (n = 0; n < maxlen; ++n) {
if((rc=rio_read(rp,&c,1))==1){
*bufp++=c;
if(c=='\n'){
break;
}
}else if(0==rc){
if(1==n){
return 0;//EOF no data read
} else{
break;//EOF,some data was read
}
} else{
return -1;//error
}
}
*bufp=0;
return n;
}
void check_clients(pool *p){
int i,connfd,n;
char buf[MAXLINE];
rio_t rio;
for(i=0;(i<=p->maxi)&&(p->nready>0);i++){
connfd=p->clientfd[i];
rio=p->clientrio[i];
if((connfd>0)&&(FD_ISSET(connfd,&p->ready_set))){
p->nready--;
if((n=rio_readlineb(&rio,buf,MAXLINE))!=0){ //有新的数据,echo回去
byte_cnt+=n;
printf("Server received %d(%d total)bytes on fd %d\n",
n,byte_cnt,connfd);
rio_write(connfd,buf,n);
} else{ //没有数据则关闭并清除标记
Close(connfd);
FD_CLR(connfd,&p->read_set);
p->clientfd[i]=-1;
}
}
}
}
int main(int argc,char **argv) {
int listenfd,connfd,port;
socklen_t clientleng=sizeof(struct sockaddr_in);
static pool pool; //作为server端用于存放所有client连接
if(argc!=2){
fprintf(stderr,'usage:%s \n' ,argv[0]);
return 0;
}
port=axoi(argv[1]);
listenfd=open_listenfd(port);
init_pool(listenfd,&pool);
while (1){
pool.ready_set=pool.read_set;
pool.nready=Select(pool.maxfd+1,&pool.ready_set,NULL,NULL,NULL);
if(FD_ISSET(listenfd,&pool.ready_set)){ //当有新的链接时,把链接加入池子
connfd=Accept(listenfd,(SA *)&clientaddr,&clientlen);
add_client(connfd,&pool);
}
check_clients(&pool); //处理所有ready的链接
}
}
int open_listenfd(int port) {
int listenfd,optval=1;
struct sockaddr_in serveraddr;
if((listenfd=socket(AF_INET,SOCK_STREAM,0))<0){
return -1;
}
if(setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,(const void *)&optval,sizeof(int ))<0){
return -1;
}
bzero((char *)&serveraddr,sizeof(serveraddr));
serveraddr.sin_family=AF_INET;
serveraddr.sin_addr.s_addr=htonl(INADDR_ANY);
serveraddr.sin_port=htons((unsigned short )port);
if(bind(listenfd,&serveraddr,sizeof(serveraddr))<0){
return -1;
}
if(listen(listenfd,LISTENQ)<0){
return -1;
}
return listenfd;
}
采用EventDriven的优点
采用EventDriven的缺点
各thread独立的:thread id,stack,stack pointer,program counter,general-purpose registers,condition codes
各thread共享的:process virtual address space(code,data,heap,shared lib,open files)
因为thread context比process context更小,所以在switch时更快。另一个线程和进程的区别是线程没有严格的继承树
#include
typedef void *(func)(void *);
//return 0 if ok,nonzero on error
int pthread_create(pthread_t *tid,pthread_attr_t *attr,func *f,void *arg);
//return thread id of caller
pthread_t pthread_self(void)
//return 0 if ok,nonzero on error
void pthread_exit(void *thread_return);
//return 0 if ok,nonzero on error
int pthread_cancel(pthread_t tid);
//return 0 if ok,nonzero on error
int pthread_join(pthread_t tid,void **thread_return);
//return 0 if ok,nonzero on error
int pthread_detach(pthread_t tid);
pthread_once_t once_control=PTHREAD_ONCE_INIT;
//always return 0
int pthread_once(pthread_once_t *once_control,void (*init_routine)(void));
pthread_create用于创建线程,需要注意func的签名,入参和出参必须都是void *,然后func内部再做强制类型转换
pthread_exit用于结束自己(如果是main调用该方法,会等待其他所有线程结束),而pthread_cancel用于结束其他线程。
pthread_detash是将一个线程由joinable转化为detached,对于joinable的线程而言,其结束时需要由其他线程通过pthread_join来执行reap操作,但是对于detached的线程而言则无需。而这个操作一般由被创建的线程在开始时通过*pthread_detash(pthread_self())*来实现
#include
#include
#include
#include
#include
#include
void *thread(void *vargp){
int connfd=*((int *)vargp);
pthread_detach(pthread_self());
free(vargp);
echo(connfd);
close(connfd);
return NULL;
}
int main(int argc, char **argv) {
int listenfd,*connfdp,port;
socklen_t clientlen=sizeof(struct sockaddr_in);
struct sockaddr_in clientaddr;
pthread_t tid;
if(2!=argc){
fprintf(stderr,"usage:%s \n" ,argv[0]);
}
port=atoi(argv[1]);
listenfd=open_listenfd(port);
while (1){
connfdp=malloc(sizeof(int ));
*connfdp=accept(listenfd, (struct sockaddr *) &clientaddr, &clientlen);
pthread_create(&tid,NULL,thread,connfdp);
}
}
上面代码还体现出多进程和多线程的区别在于多进程中各个进程在代码执行前需要各自关闭fd,而多线程不需要
此处需要注意的是main thread为每个peer thread分配了空间存放listenfd而不是下面传递地址的方法,这是因为当pthrea_create执行后,也许子线程还未来得及读取main就更改了connfd的值
connfd=accept(listenfd,&clientaddr,&clientlen);
pthread_create(&tid,NULL,thread,&connfd);
对于多线程下的变量一般分为以下几类,他们的内存共享情况各有不同
把并行的线程的各条指令依次放置到X,Y坐标上形成如下二维表格,可以表明两个线程的指令执行关系,每条指令的执行会向上或向右执行一步。
我们把操作多线程共享变量的操作称为critical section,同于同一个变量,只能有一个线程处于critical section中,这个被称为mutually exclusive access,在progress graph中两个critical section定义了一个unsafe region(如下图所示),注意这个region是不包含边界的(虚线表示),未进入unsafe region的轨迹称为safe trajectory,否者称为unsafe trajectory。
semaphore是一个只能进行如下操作的非负整形变量
由上面两个原子操作可以保证s一定是非0数值,这一特性称为semaphore invariant,Posix定义了如下一些操作
#include
int sem_init(sem_t *sem,0,unsigned int value);
int sem_wait(sem_t *s);//p
int sem_post(sem_t *s);//v
人们把一种取值范围只能是0和1的semaphore称为binary semaphore,用来保证线程安全,称为mutexes,对于P操作称为locking,V操作称为unlocking,如果一个线程执行了lock但是没有执行unlock则称为holding这个mutex,mutex初始值设为1表示未上锁。
通过下面代码中的P和V得到一个forbidden region来实现mutual exclusion(图中的点表示了s的值)
volatile int cnt=0;
sem_t mutex;
Sem_init(&mutex,0,1);//mutex=1
for(i=0;i<niters;i++){
P(&mutex);
cnt++;
V(&mutex);
}
人们把给一些资源用来计数的semaphore称为counting semaphone
生产者消费者模型中生产者和消费者通过一个buffer来连接,生产者和生产者需要竞争空的slot(在初始状态下有n个),而消费者之间需要竞争获取资源(在初始状态下有0个),所以这个时候外边一层的PV语法提供了一种安排,以确保通过了P之后的生产者/消费者一定能操作。但是第一个P只是保证了充足的资源,但是没有避免上面提到的并发问题,所以还需要一堆PV来隔离。
typedef struct{
int *buf; //buffer array
int n; //slot数量
int front; //buf[(front+1)%n]第一个item
int rear; //buf[rear&n]最后一个item
sem_t mutex; //用于互斥
sem_t slots; //可用slot
sem_t items; //可用item
} sbuf_t;
//初始化
void sbuf_init(sbuf_t *sp,int n){
sp->buf=Calloc(n,sizeof(int));//动态分配内存
sp->n=n
sp->front=sp->rear=0;
Sem_init(&sp->mutex,0,1);
Sem_init(&sp->slots,0,n);
Sem_init(&sp->items,0,0);
}
//clean
void suf_deinit(sbuf_t *sp){
free(sp->buf);
}
//添加到尾
void sbuf_insert(sbuf_t *sp,int item){
P(&sp->slots); //等到有充足的slot
P(&sp->mutex); //互斥
sp->buf[(++sp->rear)%(sp->n)]=item;
V(&sp->mutex);
V(&sp->items); //声明有充足的item
}
//从头取出
int sbuf_remove(sbuf_t *sp){
int item;
P(&sp->items);
P(&sp->mutex);
item=sp->buf[(++sp->front)%(sp->n)];
V(&sp->mutex);
V(&sp->slots);
return item;
}
这个问题的解决原则可以分为读者优先和写者优先,对于前者而言,除非正在写,否者读者不会等待,而对于后者是为了尽快的写入,所以当有写者在等待时,后续到达的所有读者都需要等待。
下面是读者优先的方案,这里把所有读者视为一起(因为他们之间没有互斥要求),然后使用w来完成一各读者群和多个写者的互斥,读者内部通过readercnt来决定是否获取w,但是readercnt的操作本身也需要加锁,所以出现了mutex来保护readercnt
int readcnt; //initially 0
sem_t mutex,w; //both initially 1
void reader(void){
while(1){
P(&mutex);
readercnt++;
if(readcnt==1){
P(&w);
}
V(&mutex);
// do read
P(&mutex);
readcnt--;
if(readcnt==0){
V(&w);
}
V(&mutex);
}
}
void writer(void){
while(1){
P(&w);
//do write
V(&w);
}
}
由于在接受请求时每次创建新的线程代价过于高昂,一种做法是main线程创建多个woker线程,然后main接受请求后通过producer-consumer模式将fd分发给worker,再由worker处理后返回。
a sequential program is written as a single logical flow.
a concurrent program is written as multiple concurrent flows;
a parallel program is a concurrent program running on multiple processors;
所以他们的关系可以使用下图来表示
为了衡量并行程序的效果,定义 S p = T 1 T p S_p=\frac{T_1}{T_p} Sp=TpT1( T k T_k Tk表示在 k k k各个core上运行的时间)作为speedup,这一指标也称为strong scaling,如果 T 1 T_1 T1是sequential programs,则 S p S_p Sp称为absolute speedup,如果 T 1 T_1 T1是parallel programs,则 S p S_p Sp称为relative speedup。
使用relative speedup有时会得到错误的结论,因为对于 T 1 T_1 T1而言需要大量的上下文切换。而使用absolute speedup则比较困难,因为需要两份代码。
另一个衡量并行程序的指标是efficiency,定义为 E p = S p p = T 1 p T p E_p=\frac{S_p}{p}=\frac{T_1}{pT_p} Ep=pSp=pTpT1,越高的 E p E_p Ep说明并行越高效,用于同步的时间少
还有一种称为weak scaling的指标,它是指如果问题和核心同步增长后所需时间不变,则efficiency为100%。所以weak scaling是一个更好的指标,因为它与我们用更多机器完成更多工作的愿望一致
如果一个函数在并发程序下调用能获得正确的结果则称为thread-safe,否者称为thread-unsafe,对于thread-unsafe的函数有以下几类
书中所指的reentrant function是一种不使用任何共享数据的函数,所以是thread-safe function的一种。解决上面第二种问题的一个解决方案就是通过把状态改为入参从而改写为reentrant function
如果一个函数同时满足以下两个条件则是reentrant function:
如果无法达到上面第一个条件,则需要调用方保证指针不被共享。这个时候也可以称为reentrant function
在C libaray中大部分的函数是线程安全的,有少部分不安全的,但是也提供了加了后缀_r的线程安全的版本。例如ctime的线程安全版本就是ctime_r
race是指多线程的执行有了暗含的执行顺序,而这是不可能的。所以代码能否正确执行取决于kernal如何来做调度。下面是一个例子,可以发现由于在创建线程时必须传递指针,而这打破了reentrant function的第一条要求,那是改进办法就是为每一个线程创建单独的指针入参。
# define N 4
void *thread(void * vargp);
int main(){
pthread_t tid[N];
int i;
for(i=0;i<N;i++){
//thread-unsafe
Pthread_create(&tid[i],NULL,thread,&i);
//thread-safe
ptr=malloc(sizeof(int));
*ptr=i;
Pthread_create(&tid[i],NULL,thread,ptr);
//end
}
for(i=0;i<N;i++){
pthread_join(tid[i],NULL);
}
exit(0);
}
void *thread(void *vargp){
int myid=*((int *)vargp);
//thread-safe版本需要下面一行
Free(vargp);
printf("hello from thread %d\n",myid);
return NULL;
}
在我看来把死锁表达的清晰的莫过于下图,一旦进入deadlock region就永远无法出去了。
deadlock一般涉及到多个mutex,避免比较麻烦,一种简便方法就是却报大家按同样顺序加锁,结果会呈现出下图