这也是一本很出名的书,在很早的时候读过一些,这次从后面开始读,看有没有新的体会。
如果逻辑流在时间上重叠,那么他们就是并发的,硬件异常处理程序、进程和UNIX信号处理程序都是熟悉的例子。并发现象不仅在内核中存在,在应用级别的程序中也存在。
访问慢速的I/O设备
与人交互
通过推迟工作以降低延迟
服务多个网络客户端
在多核机器上进行并行计算
进程
I/O多路复用
线程
构造并发程序最简单的方法就是使用进程,使用fork()等一些列的函数。看一个使用进程并发的实例:
/* * echoserverp.c - A concurrent echo server based on processes */ /* $begin echoserverpmain */ #include "csapp.h" void echo(int connfd); void sigchld_handler(int sig) { while (waitpid(-1, 0, WNOHANG) > 0) ; return; } int main(int argc, char **argv) { int listenfd, connfd, port, clientlen=sizeof(struct sockaddr_in); struct sockaddr_in clientaddr; if (argc != 2) { fprintf(stderr, "usage: %s <port>\n", argv[0]); exit(0); } port = atoi(argv[1]); Signal(SIGCHLD, sigchld_handler); listenfd = Open_listenfd(port); while (1) { connfd = Accept(listenfd, (SA *) &clientaddr, &clientlen); if (Fork() == 0) { Close(listenfd); /* Child closes its listening socket */ echo(connfd); /* Child services client */ Close(connfd); /* Child closes connection with client */ exit(0); /* Child exits */ } Close(connfd); /* Parent closes connected socket (important!) */ } } /* $end echoserverpmain */
(有时候就禁不住吐槽一下,大师的代码就是这么帅,在最少的代码量的条件下完成的那么完善,结构那么严谨,让人看着那么舒服)
首先是本地处于监听状态,然后去接受客户端的链接,一旦链接,父进程关闭创建的链接描述符,子进程关闭父进程的监听描述符。子进程处理操作,然后关闭链接。
父子进程间共享状态信息,进程有一个非常清晰的模型:共享文件表,但不共享用户地址空间。这是优点也是缺点:一个进程不可能不小心覆盖另一个进程的虚拟存储器;但是为了共享信息,他们必须使用显示的IPC机制,另一个缺点就是,他们往往比较慢,因为进程控制可IPC的开销很高。
(ps:UNIX ipc通常指的是所有允许进程和同一台主机上其他进程进行通信的技术。其中包括管道、先进先出、系统V共享存储器以及系统V信号量,这些内容也就是还没有读的那本:UNIX网络编程卷二:进程间通信)
关于I/O多路复用技术,在别的书中已经介绍了很多,在这里只是给一个例子说明。select函数处理类型为fd_set的集合,也叫描述符集合。逻辑上,我们将描述符结合看成一个大小为n的位向量,每n位对应于描述符n,当且仅当第n位为1时,描述符n才表明是描述符集合的而一个元素。
假设我们只考虑可读的描述符,select函数有两个输入:一个称为读集合的描述符集合和该读集合的技术n。select函数会一直阻塞,直到读集合中至少有一个描述符准备好可以读。当且仅当一个从该描述符读取一个字节的请求不会阻塞时,描述符k就表示准备好可以读了。作为一个副作用,select修改了参数fdset指向的fd_set,指明读集合中一个称为准备好集合的子集,这个集合是由读集合中准备好可以读了的描述符组成的。函数返回值指明了准备好集合的基数。由于这个副作用,每次调用select时都更新读集合。这个副作用在下面程序中的体现就是必须在while(1)循环中每次有描述符集合的重新复制,或者在while循环中存在FD_ZERO(&read_set),然后FD_SET(listenfd,&read_set).
要理解这个函数的思想,是一个n位的向量。在向量的某一位代表的是相应的套接字描述符,当这个可读或者可写的时候,这个向量上对应的位变为0,说明这个描述符不再这个向量中,在再次循环的时候,需要将这个描述符再次添加到这个描述符集合中。
/* $begin select */ #include "csapp.h" void echo(int connfd); void command(void); int main(int argc, char **argv) { int listenfd, connfd, port, clientlen = sizeof(struct sockaddr_in); struct sockaddr_in clientaddr; fd_set read_set, ready_set; if (argc != 2) { fprintf(stderr, "usage: %s <port>\n", argv[0]); exit(0); } port = atoi(argv[1]); listenfd = Open_listenfd(port); FD_ZERO(&read_set); FD_SET(STDIN_FILENO, &read_set); FD_SET(listenfd, &read_set); while (1) { ready_set = read_set; Select(listenfd+1, &ready_set, NULL, NULL, NULL); if (FD_ISSET(STDIN_FILENO, &ready_set)) command(); /* read command line from stdin */ if (FD_ISSET(listenfd, &ready_set)) { connfd = Accept(listenfd, (SA *)&clientaddr, &clientlen); echo(connfd); /* echo client input until EOF */ } } } void command(void) { char buf[MAXLINE]; if (!Fgets(buf, MAXLINE, stdin)) exit(0); /* EOF */ printf("%s", buf); /* Process the input command */ } /* $end select */当然,这个函数只是展示了多路复用的思想,对于这个程序来说,一旦链接到某个客户端,就会连续会送输入行,直到客户端关闭这个连接中它的那一端。因此,如果你键入一个命令到标准输入,将不会得到相应,直到服务器和客户端之间结束。一个更好的方法是更细粒度的多路复用,服务器每次循环回送一个问本行。
线程的并发编程是上述两种方式的混合,使用了多个逻辑流,但是在一个地址空间中,线程是运行在进程上下文中的逻辑流,线程由内核自动调度。每个线程都有它自己的线程上下文,包括一个唯一的整数线程ID、栈、栈指针、程序计数器,通用目的寄存器和条件码。
关于线程的相关可以参考:
http://blog.csdn.net/yusiguyuan/article/details/12405953
http://blog.csdn.net/yusiguyuan/article/details/12154823
多个线程运行单一进程的上下文中,因此共享这个进程虚拟地址空间按的整个内容,包括他的代码、数据、堆、共享库和打开的文件。
线程的执行有别与进程,一个线程上下文切换要比进程上下文切换快得多。和线程相关的还有线程的终止、回收已终止线程的资源、分离线程。
在任何一个时间点上,线程是可结合的或者是可分离的。一个可结合的线程能够被其他线程收回其资源和杀死。在被其他线程回收之前,它的存储器资源是没有被释放的。相反,一个分离的线程时不能被其他线程回收或杀死的,它的存储器资源在它终止时由系统自动释放的。下面看一个例子:
/* * echoservert.c - A concurrent echo server using threads */ /* $begin echoservertmain */ #include "csapp.h" void echo(int connfd); void *thread(void *vargp); int main(int argc, char **argv) { int listenfd, *connfdp, port, clientlen=sizeof(struct sockaddr_in); struct sockaddr_in clientaddr; pthread_t tid; if (argc != 2) { fprintf(stderr, "usage: %s <port>\n", argv[0]); exit(0); } port = atoi(argv[1]); listenfd = Open_listenfd(port); while (1) { connfdp = Malloc(sizeof(int)); *connfdp = Accept(listenfd, (SA *) &clientaddr, &clientlen); Pthread_create(&tid, NULL, thread, connfdp); } } /* thread routine */ void *thread(void *vargp) { int connfd = *((int *)vargp); Pthread_detach(pthread_self()); Free(vargp); echo(connfd); Close(connfd); return NULL; } /* $end echoservertmain */首先,在21~22行,在传递给线程的参数时,是每个进程自己分配的空间按,如果使用的是主线程分配的空间,那么这个线程接受到参数在下一个连接建立之后,就说明这个连接是错误的。
同时分离了每个线程,这样保证线程在终止后会被系统自己回收资源。
优秀的程序每句代码都透射着严谨!!
一个变量时共享的,当且仅当这个线程引用这个变量的某个实例。
/* $begin sharing */ #include "csapp.h" #define N 2 void *thread(void *vargp); char **ptr; /* global variable */ int main() { int i; pthread_t tid; char *msgs[N] = { "Hello from foo", "Hello from bar" }; ptr = msgs; for (i = 0; i < N; i++) Pthread_create(&tid, NULL, thread, (void *)i); Pthread_exit(NULL); } void *thread(void *vargp) { int myid = (int)vargp; static int cnt = 0; printf("[%d]: %s (cnt=%d)\n", myid, ptr[myid], ++cnt); } /* $end sharing */然我们来分析下这个程序,创建两个线程,主线程传递一个唯一的ID给每个线程,每个线程利用这个ID输出一条个性化的信息,以及调用该线程例程的总次数。看看到底哪些是共享的,哪些是私用的,哪些是可以更改的。
共享变量非常方便,但是引入了同步错误的可能性。
/* * badcnt.c - An improperly synchronized counter program */ /* $begin badcnt */ #include "csapp.h" #define NITERS 200000000 void *count(void *arg); /* shared counter variable */ unsigned int cnt = 0; int main() { pthread_t tid1, tid2; Pthread_create(&tid1, NULL, count, NULL); Pthread_create(&tid2, NULL, count, NULL); Pthread_join(tid1, NULL); Pthread_join(tid2, NULL); if (cnt != (unsigned)NITERS*2) printf("BOOM! cnt=%d\n", cnt); else printf("OK cnt=%d\n", cnt); exit(0); } /* thread routine */ void *count(void *arg) { int i; for (i = 0; i < NITERS; i++) cnt++; return NULL; } /* $end badcnt */
这个错误会引发操作系统中很多很重要的问题:互斥、PV操作等等。
可以参考这个
无论哪种并发机制,同步对共享数据的并发访问都是一个困难的问题。提出对信号量的P和V操作就是为了帮助解决这个问题。信号量操作可以用来提供对共享数据的互斥访问,也对诸如生产者-消费者程序中有限缓冲区和读者-写者系统中的共享对象这样的资源访问进行调度。
书中还有一段话值得回味,信号量提供了一种很方便的方法来确保对共享变量的互斥访问。基本思想是将每个共享变量(或者一组相关的共享变量)与一个信号量s(初始为1)联系起来,然后用P(s)和V(s)操作将相应的临界区包围起来。以这种方式来保护共享变量的信号量叫做二元信号量。因为它的值总是0或者1.以提供互斥为目的的二元信号量常常也称为互斥锁。
除了提供互斥之外,信号量的另一个重要作用是调度对共享资源的访问。在这种场景下,一个线程用信号量操作来通知另一个线程,程序状态中的某个条件已经为真了。两个经典而有用的例子是生产者-消费者和读者-写者问题。
PS:在开头想引用孔夫子的话:学而不思则罔,思而不学则殆!!觉得这句话对于CSer再合适不过,大概宽泛的想一下,计算机的技术太多太多,从上层应用下层开发,不管是那一块学精通都可以混一辈子饭吃!!但是总想什么都会还是不现实的,所以还是跟着自己的兴趣来,还有就是对一些东西必须去思考去理解!!!思考理解!!!!