zoukankan      html  css  js  c++  java
  • 20145210《信息安全系统设计基础》第13周学习总结

    20145210《信息安全系统设计基础》第13周学习总结

    第十一章 网络编程

    客户端-服务器编程模型

    ·一个应用是由一个服务器进程和一个或多个客户端进程组成
    ·客户端-服务器模型中最基本的操作是事务,由四步组成:
    ·当一个客户端需要服务时,向服务器发送一个请求,发起一个事务。
    ·服务器收到请求后,解释它,并以适当的方式操作它的资源。
    ·服务器给客户端发送一个相应,并等待下一个请求。
    ·客户端收到响应并处理它。
    ·客户端和服务器都是进程

    网络

    ·物理上,网络是一个按照地理远近组成的层次系统:最低层是LAN(局域网),最流行的局域网技术是以太网
    ·一台主机可以发送一段位,称为帧,到这个网段内其它任何主机。每个帧包括一些固定数量的头部位(标识此帧的源和目的地址及帧长)和数据位(有效载荷)。每个主机都能看到这个帧,但是只有目的主机能读取
    ·使用电缆和网桥,多个以太网段可以连接成较大的局域网,称为桥接以太网。这些电缆的带宽可以是不同的。
    ·互联网重要特性:由采用不同技术,互不兼容的局域网和广域网组成,并能使其相互通信
    ·不同网络相互通信的解决办法:一层运行在每台主机和路由器上的协议软件,消除不同网络的差异
    ·协议提供的两种基本能力:
    ·命名机制:唯一的标示一台主机
    ·传送机制:定义一种把数据位捆扎成不连续的片的同一方式

    全球IP因特网

    ·每台因特网主机都运行实现TCP/IP协议,因特网的客户端和服务器混合使用套接字接口函数和Unix I/O函数进行通信

    ·把因特网看做一个世界范围的主机集合,满足以下特性:
    ·主机集合被映射为一组32位的IP地址
    ·这组IP地址被映射为一组称为因特网域名的标识符
    ·因特网主机上的进程能够通过连接和任何其他主机上的进程
    ·在IP地址结构中存放的地址总是以(大端法)网络字节顺序存放的,主机字节顺序是小端法
    ·IP地址通常是以一种称为点分十进制表示法来表示的,每个字节由他的十进制表示,并且用句点和其他字节间分开
    ·虚拟机的点分十进制地址:

    ·检索并打印一个DNS主机条目:

    #include "csapp.h"
    int main(int argc, char **argv) 
    {
        char **pp;
        struct in_addr addr;
        struct hostent *hostp;
        if (argc != 2) {
        fprintf(stderr, "usage: %s <domain name or dotted-decimal>
    ", 
            argv[0]);
        exit(0);
        }
        if (inet_aton(argv[1], &addr) != 0) 
        hostp = Gethostbyaddr((const char *)&addr, sizeof(addr), AF_INET); 
        else                                
        hostp = Gethostbyname(argv[1]);
        printf("official hostname: %s
    ", hostp->h_name);
        for (pp = hostp->h_aliases; *pp != NULL; pp++)
        printf("alias: %s
    ", *pp);
        for (pp = hostp->h_addr_list; *pp != NULL; pp++) {
        addr.s_addr = ((struct in_addr *)*pp)->s_addr;
        printf("address: %s
    ", inet_ntoa(addr));
        }
        exit(0);
    }
    

    本地回送地址:

    多个域名映射为同一个IP地址:

    多个域名映射到多个IP地址:

    ·因特网客户端和服务器通过在连接上发送和接收字节流来通信,从连接一对进程的意义上而言,连接是点对点的,从数据可以同时双向流动的角度来说,他是全双工的,从由源进程发出的字节流最终被目的进程以他发出的顺序收到他的角度来说,他是可靠的
    ·一个套接字是连接的一个端点,每个套接字都有相应的套接字地址,用“地址:端口”来表示
    ·当客户端发起一个连接请求时,客户端套接字地址中的端口是由内核自动分配的,称为临时端口
    ·一个连接是由他两端的套接字地址唯一确定的,这对套接字地址叫做套接字对

    套接字接口

    ·套接字接口是一组用来结合unit I/O函数创建网络应用的函数

    ·一个套接字就是通信的一个端点,套接字就是一个有相应描述符的打开文件
    ·_in后缀是互联网络的缩写i额,不是输入的缩写
    ·函数:

    ·socket函数:创建套接字描述符
    ·connect函数:建立和服务器的连接
    ·open_clientfd函数:客户端用该函数和服务器建立连接
    ·bind函数:被服务器用来和客户端建立连接
    ·listen函数:告诉内核描述符是被服务器而不是客户端使用的
    ·open_listenfd函数:服务器用来创建一个监听描述符
    ·accept函数:服务器等待来自客户端的连接请求
    

    Web服务器

    ·Web客户端和服务器之间的交互用的是一个基于文本的应用级协议,叫做 HTTP
    ·HTTP 是一个简单的协议。一个 Web 客户端(即浏览器) 打开一个到服务器的因特网连接,并且请求某些内容。服务器响应所请求的内容,然后关闭连接。浏览器读取这些内容,并把它显示在屏幕上
    ·Web服务器和常规文件检索服务的区别:Web内容可以用一种叫做 HTML的语言来编写。一个 HTML 程序(页)包含指令(标记),它们告诉浏览器如何显示这页中的各种文本和图形对象
    ·Web 服务器以两种不同的方式向客户端提供内容:
    ·取一个磁盘文件,并将它的内容返回给客户端。磁盘文件称为静态内容, 而返回文件给客户端的过程称为服务静态内容
    ·运行一个可执行文件,并将它的输出返回给客户端。运行时可执行文件产生的输出称为态内容,而运行程序并返回它的输出到客户端的过程称为服务动态内容
    ·HTTP请求的组成:一个请求行,后面跟随零个或更多个请求报头,再跟随一个空的文本行来终止报头列表
    ·HTTP响应的组成:一个响应行,后面跟随零个或更多个响应报头,再跟随一个终止报头的空行,再跟随一个响应主体

    第十二章 并发编程

    ·并发:如果逻辑控制在时间上重叠,那么他们就是并发的,并发不仅仅局限于内核,它也可以在应用程序中扮演重要角色

    进程

    ·进程:
    ·每个控制逻辑流都是一个进程,由内核来调度和维护
    ·进程有独立的虚拟地址空间
    ·控制流必须使用某种显示的进程间通信机制来和其他流通信
    ·构造并发程序最简单的方法就是用进程
    ·构造并发服务器:在父进程中接受客户端连接请求,然后创建一个新的子进程来为每个新客户端提供服务
    具体步骤:现有两个客户端和一个服务器,服务器正在监听一个监听描述符上的连接请求,现假设服务器接受了客户端1的连接请求,并返回一个已连接描述符
    (1)在接受连接之后,服务器派生一个子进程,这个子进程获得服务器描述符表的完整拷贝
    (2)子进程关闭它的拷贝监听中的描述符3,而父进程关闭他的已连接描述符4的拷贝(因为父、子进程中的已连接描述符都指向同一个文件表选项,所以父进程关闭他的已连接描述符是至关重要的,否则,将永远不会释放已连接描述符的文件表条目,而且由此引起的存储器泄露将最终消耗尽可用的存储器,使系统崩溃)

    (3)假设父进程为客户端1创建了子进程之后,他接受一个新的客户端2的连接请求,并返回一个新的已连接描述符5

    (4)父进程又派生另一个子进程,这个子进程用已连接描述符5为它的客户端提供服务

    (5)此时父进程正在等待下一个连接请求,而两个子进程正在并发地为他们各自的客户端提供服务

    ·回收僵死进程:通常服务器会运行很长时间,必须要回收僵死子进程的资源
    ·父进程派生一个子进程来处理每个新的连接请求,使用基于进程的并发服务器echo,代码如下:

    #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;
    socklen_t clientlen=sizeof(struct sockaddr_in);
    struct sockaddr_in clientaddr;
    
    if(argc !=2){
    fprintf(stderr,"usage:%s<port>
    ",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);
    echo(connfd);
    Close(connfd);  //关闭子进程的connfd拷贝
    exit(0);
    }
    Close(connfd);  //关闭父进程的connfd拷贝
    }
    }
    
    

    ·在父、子进程中共享信息:共享文件表,但是不共享用户地址空间
    ·进程有独立地址空间的优缺点

    优点:一个进程不会覆盖另一个进程的虚拟存储器
    缺点:
    ·独立的地址空间使得进程共享状态信息变得更加困难
    ·显示的IPC机制比较慢,进程控制和IPC的开销很高
    

    I/O多路复用

    ·服务器必须响应两个相互独立的I/O事件:

    ·网络客户端发起请求
    ·用户在键盘上键入命令行
    

    ·I/O多路复用技术:使用select函数,要求内核挂起进程,只有一个或多个I/O事件发生后,才将控制返还给应用程序
    ·select函数:描述符集合

    #include<unistd.h>
    #include<sys/types.h>
    
    int select(int n,fd_set *fdset,NULL,NULL,NULL); //返回已准备好的描述符的非零的个数,若出错则为-1,n为任何描述符集合的最大基数
    
    FD_ZERO(fd_set *fdset);
    FD_CLR(int fd,fd_set *fdset);
    FD_SET(int fd,fd_set *fdset);  //读集合
    FD_ISSET(int fd,fd_set *fdset);
    

    ·每次调用select时都更新读集合的原因:select修改了参数fdset指向的fd_set,指明读集合中一个称为准备好集合的子集,函数的返回值指明了准备好集合的基数,因此,必须在每次调用select时都更新读集合
    ·服务器使用select等待监听描述符上的连接请求和标准输入上的命令,使用I/O多路复用echo服务器,代码如下:

    #include "csapp.h"
    void echo(int connfd);
    void command(void);
    
    int main(int argc,char **argv)
    {
    int listenfd,connfd,port;
    socklen_t clientlen = sizeof(struct sockaddr_in);
    struct sockaddr_in clientaddr;
    fd_set read_st,ready_set;
    
    if(argc !=2)
    {
    fprintf(stderr,"usage:%s<port>
    ",argv[0]);
    exit(0);
    }
    
    port = atoi(argv[1]);
    listenfd = Open_listenfd(port);  //打开一个监听描述符
    
    FD_ZERO(&read_set);
    FD_SET(STDIN_FILENO,&read_set);  //标准输入(描述符0)
    FD_SET(listenfd,&read_set);  //监听描述符(描述符3)
    
    while(1)
    {
    ready_set=read_set;
    Select(listenfd+1,&ready_set,NULL,NULL,NULL);  
    if(FD_ISSET(STDIN_FILENO,&ready_set))  //已有标准输入
    command();  //返回程序前读、解析、响应命令
    if(FD_ISSET(listenfd,&ready_set))  //监听描述符准备好了
    {
    connfd=Accept(listenfd,(SA *)&clientaddr,&clientlen);  //得到一个已连接描述符
    echo(connfd);  //将来自客户端的每一行送回去,直到客户端关闭连接中它的那一端
    Close(connfd);
    }
    }
    }
    
    void command(void)
    {
    char buf[MAXLINE];
    if (!Fgets(buf,MAXLINE,stdin))
    exit(0);
    printf("%s",buf);
    }
    
    

    ·I/O多路复用可以用做并发事件驱动程序的基础,在事件驱动程序中,流是因为某种事件而前进的,一般概念是把逻辑流模型化为状态机。一个状态机就是一组状态、输入事件和转移
    ·一个状态机从某种初始状态开始执行,每个输入事件都会引发一个从当前状态到下一状态的转移
    ·并发事件驱动程序中echo服务器中逻辑流的状态机如下:

    ·服务器使用I/O多路复用,借助select函数预测输入事件的发生,检测两种不同类型的输入事件:

    1.来自一个新客户端的连接请求到达
    2.一个已存在的客户端的已连接描述符准备好可以读了
    

    ·基于i/o多路复用的并发echo服务器代码如下:

    #include"csapp.h"
    
    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 main(int argc,char **argv)
    {
    int listenfd,connfd,port;
    socklen_t clientlen = sizeof(struct sockaddr_in);
    struct sockaddr_in clientaddr;
    static pool;
    
    if(argc !=2)
    {
    fprintf(stderr,"usage:%s<port>
    ",argv[0]);
    exit(0);
    }
    port = atoi(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);  //把来自每个准备好的已连接描述符的一个文本行送回去
    }
    }
    
    

    ·init_pool函数初始化客户端池,代码如下:

    void init_pool(int listenfd,pool *p)  //初始化客户端池
    {
    int i;
    p->maxi=-1;
    for(i=0;i<FD_SRTSIZE;i++)
    p->clientfd[i]=-1;
    
    p->maxfd=listenfd;
    FD_ZERO(&p->read_set);
    FD_SET(listenfd,&p->read_set);
    }
    
    

    ·add_client函数添加一个新的客户端到活动客户端池中,代码如下:

    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->cientrio[i],confd);  //初始化RIO读缓冲区
    
    FD_SET(connfd,&p->read_set);  //将已连接描述符添加到select读集合
    
    if(connfd > p->maxfd)
    p->maxfd = connfd;  //记录select最大文件描述符
    if(i > p->maxi)
    p->maxi=i;  //记录到clientfd数组的最大索引
    break;
    }
    if(i==FD_SETSIZE)
    app_error("add_client error:Too many clients")
    }
    
    

    ·check_clients函数回送来自每个准备好的已连接描述符的一个文本行,代码如下:

    void check_client(pool *p)
    {
    int i,connfdn;
    char buf[MAXLINE];
    rio_t rio;
    
    for(i=0;(i <= p->maxi)&&(p->nready > 0);i++)
    {
    connfd = p->clientfd[i];
    rio = p->clienrio[i];
    
    if((connfd>0)&&(FD_ISSET(connfd,&p->ready_set)))
    {
    p->nready--;
    if((n==Rio_readlineb(&rio,buf,MAXLINE))!=0)
    {
    byte_cnt += n;
    printf("Sever recevied %d (%d total) bytes on fd %d
    ",n,byte_cnt,connfd);
    Rio_writen(connfd,buf,n);
    }
    
    else{
    Close(confd);
    FD_CLR(connfd,&p->read_set);
    p->clientfd[i]=-1;
    }
    }
    }
    }
    
    

    ·事件驱动设计的优缺点:

    优点:
    ·比基于进程的设计给了程序员更多的对程序行为的控制
    ·一个基于i/o多路复用的事件驱动服务器是运行在单一进程上下文中的,因此每个逻辑流都能访问该进程的全部地址空间,这使得在流之间共享数据变得很容易
    ·可以利用熟悉的调试工具来调试你的并发服务器,事件驱动设计常常比基于进程的设计要高效得多,因为他们不需要进程上下文切换来调度新的流
    缺点:
    ·编码复杂
    ·随着并发粒度的减小,复杂性上升
    ·不能充分利用多核处理器
    

    线程

    ·线程:运行在进程上下文之间的逻辑流,每个线程都有自己的线程上下文,所有的运行在一个进程里的线程共享该进程的整个虚拟地址空间,包括它的代码、数据、堆、共享库和打开的文件
    ·线程上下文:
    ·唯一的整数线程ID(TID)
    ·栈
    ·栈指针
    ·程序计数器
    ·通用目的寄存器
    ·条件码
    ·线程由内核自动调度,内核通过一个整数ID来识别线程
    ·主线程;每个进程开始生命周期时的单一线程
    ·对等线程:由主线程创建,与主线程并发运行,每个对等线程都能读写相同的共享数据
    ·控制切换到对等线程:通过上下文切换,因为主线程执行一个慢速系统调用,或者因为它被系统的间隔计时器中断

    ·主线程与其它线程的区别:

    ·主线程总是线程中第一个运行的线程
    ·一个线程可以杀死他的任何对等线程,或者等待他的任意对等线程终止
    

    ·线程执行与进程的区别:

    ·一个线程的上下文要比一个进程的上下文小得多,线程的上下文切换要比进程的上下文切换快得多
    ·线程不像进程那样,不是按照严格的父子层次来组织的,和一个进程相关的线程组成一个对等池,独立于其他线程创建的线程
    

    ·Posix线程:在C程序中处理线程的一个标准接口,允许进程创建、杀死和回收线程,与对等线程安全地共享数据,通知对等线程系统状态的变化
    ·Pthread程序代码如下:

    #include"csapp.h"
    void *thread(void *vargp);  //每个线程例程都以一个通用指针作为输入,并返回一个通用指针
    
    int main()  //主线程
    {
    pthread_t tid;  //本地变量tid用来存放对等线程的线程ID
    Pthread_create(&tid,NULL,thread,NULL); //创建一个新的对等线程,当对Pthread_create的调用返回时,主线程和新创建的对等线程同时运行,并且tid存放新线程的ID
    Pthread_join(tid,NULL); //主线程等待对等线程终止
    exit(0); //终止当时运行在这个进程中的所有进程
    }
    
    void *thread(void *vargp)  //对等线程
    {
    printf("Hello,world!
    ");
    return NULL;  //终止对等线程
    }
    
    

    ·创建线程:调用pthread_create函数:若成功则返回0,若出错则返回非零

    #include<pthread.h>
    typedef void *(func)(void *)
    
    int pthread_create(pthread_t *tid,pthread_attr_t *attr,func *f,void *arg);
    

    其中:

    ·pthread_t *tid:新创建线程的ID
    ·pthread_attr_t *attr:改变新创建线程的默认属性
    ·func *f:线程例程
    ·void *arg:输入变量
    

    ·获得自己的线程ID:调用pthread_self函数:返回调用者的线程ID

    #include<pthread.h>
    pthread_t pthread_self(void)
    

    ·终止线程:
    (1)当顶层的线程例程返回时,线程会隐式地终止
    (2)通过调用pthread_exit函数,线程会显式地终止,他会等待所有其他对等线程终止,然后再终止主线程和整个进程,若成功则返回0,若出错则为非零

    #include<pthread.h>
    void pthread_exit(void *thread_return);
    

    (3)某个对等线程调用Unix的exit函数,该函数终止进程以及所有与进程相关的线程
    (4)另一个对等线程通过以当前线程ID作为参数调用pthread_cancel函数来终止当前线程,若成功则返回0,若出错则为非零

    #include<pthread.h>
    int pthread_cancel(pthread_t tid);
    

    ·回收已终止线程的资源:调用pthread_join函数,若成功则返回0,若出错则为非零

    #include<pthread.h>
    int pthread_join(pthread_t tid,void **thread_return);
    

    pthread_join函数会阻塞,直到线程tid终止,将线程例程返回的(void*)指针赋值为thread_return指向的位置,然后回收已终止线程占用的所有存储器资源。pthread_join函数只能等待一个指定的线程终止,没有办法让pthread_join函数等待任意一个线程终止
    ·分离线程:一个可结合的线程能够被其他线程回收其资源和杀死,一格分离的线程不能被其它线程分离或者杀死,调用pthread_detach函数分离线程,若成功则返回0,若出错则为非零

    #include<pthread.h>
    int pthread_detach(pthread_t tid);
    

    ·初始化线程:pthread_once函数允许你初始化与线程例程相关的状态,函数总是返回0

    #include<pthread.h>
    
    pthread_once_t once_control = PTHREAD_ONCE_INI;
    
    int pthread_once(pthread_once_t *once_control,void(*init_routine)(void));
    

    ·基于线程的并发echo服务器:

    #include"csapp.h"
    
    void echo(int connfd);
    void *thread(void *vargp);
    
    int main(int argc,char **argv)
    {
    int listenfd,*connfd,port;
    socklen_t clientlen = sizeof(struct sockaddr_in);
    struct sockaddr_in cllientaddr;
    pthread_t tid;
    
    if(argc != 2)
    {
    fprintf(stderr,"usage:%s<port>
    ",argv[0]);
    exit(0);
    }
    port=atoi(argv[1]);
    listenfd = Open_listenfd(port);
    while(1){
    connfd=Malloc(sizeof(int));
    *connfd=Accept(listenfd,(SA *)&clientaddr,&clientlen);
    Pthread_create(&tid,NULL,thread,connfdp);  //创建一个指向已连接符的指针,将已连接描述符传递给对等线程
    }
    }
    
    void *thread(void *vargp)  //赋值给局部变量
    {
    int connfd=*((int *)vargp);  //对等线程间接引用这个指针
    Pthread_detach(pthread_self());  // 分离线程,使他在终止时他的存储器能被收回
    Free(vargp);  //释放主线程分配的存储器块
    echo(connfd);
    Close(connfd);
    return NULL;
    }
    
    
    

    ·说明共享不同方面的示例程序:

    #include"csapp.h"
    #define N 2
    void *thread(void *vargp);
    
    char **ptr; //定义全局变量
    
    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)
    ",myid,ptr[myid],++cnt);
    return NULL;
    }
    
    

    ·寄存器是从不共享的,虚拟存储器总是共享的
    ·线程化的C程序中变量根据他们的存储类型被映射到虚拟存储器:
    ·全局变量:定义在函数之外的变量,在运行时,虚拟存储器的读/写区域只包含全局变量的一格实例,任何线程都可以引用
    ·本地自动变量:定义在函数内部但是没有static属性的变量,在运行时,每个线程都包含他自己的所有本地自动变量的实例,即使当多个线程执行同一个线程例程时也是如此
    ·本地静态变量:本地静态变量是定义在函数内部并有static属性的变量,虚拟存储器的读/写区域只包含在程序中声明的每个本地静态变量的一个实例
    ·共享变量:我们说一个变量v是共享的,当且仅当他的一个实例被一个以上的线程引用

    用信号量同步线程

    ·共享变量 引入了同步错误,我们将线程i的循环代码分解成成五个部分:

    ·H:在循环头部的指令块
    ·L:加载共享变量cnt到寄存器%eax的指令,这里%eax表示线程i中的寄存器%eax的值
    ·U:更新(增加)%eax的指令
    ·S:将%eax的更新值存到共享变量cnt的指令
    ·T:循环尾部的指令块
    

    ·进度图:将n个兵法线程的执行模型化为一条n维笛卡尔空间中的轨迹线
    ·转换:进度图将指令模型化为一种从状态到另一种状态的转换,合法的转换是向右或者向上,对角线转换是不允许的,向下或者向左的转换也是不合法的
    ·互斥:每个线程在执行他的临界区中的指令时,拥有对共享变量的互斥的访问
    ·不安全区:在进度图中,两个临界区的交集形成的状态空间区称为不安全区
    ·安全轨迹线:绕开不安全区的轨迹线,任何安全轨迹线都将正确地更新共享计数器
    ·不安全轨迹线:接触到任何不安全区的轨迹线

    ·信号量:信号量s是具有非负整数值的全局变量,只能有两种特殊的操作来处理:

    P(s):
    ·s非零:P将s减一,并立即返回
    ·s为零:挂起这个线程,直到s变为非零,而一个V操作会重启这个线程,在重启之后,P操作将s减1,并将控制返回给调用者
    V(s):
    ·V操作将s加一,如果有任何线程阻塞在P操作等待s变为非零,那么V操作会重启这些线程中的一个,然后该线程将s减1,完成他的P操作
    ·当有多个线程在等待同一个信号量时,你不能预测V操作要重启哪一个线程
    

    ·P和V的定义确保了一个正在运行的程序绝不可能进入这样一种状态,也就是一个正确初始化了的信号量又一个负值,这个属性称为信号量不变性
    ·使用信号量来实现互斥:将每个共享变量与一个信号量s联系起来,然后用P(s)和V(s)操作将相应的临界区包围起来
    ·互斥锁:以提供互斥为目的的二元信号量常常也称为互斥锁
    ·加锁:在一个互斥锁上执行 P 操作称为对互斥锁加锁
    ·互斥锁解锁:执行 V操作称为对互斥锁解锁
    ·占用互斥锁:对一个互斥锁加了锁但是还没有解锁的线程称为占用这个互斥锁
    ·计数信号量:一个被用作一组可用资源的计数器的信号量称为计数信号量
    ·禁止区:这种 P 和 V操作的结合创建了一组状态, 叫做禁止区
    ·禁止区完全包括了不安全 区,阻止了实际可行的轨迹线接触到不安全区

    ·信号量的作用:(1)提供互斥(2)调度对共享资源的访问
    ·生产者—消费者问题:生产者产生项目并把他们插入到一个有限的缓冲区中,消费者从缓冲区中取出这些项目,然后消费它们
    ·读者—写者问题:
    ·读者优先,要求不让读者等待,除非已经把使用对象的权限赋予了一个写者。
    ·写者优先,要求一旦一个写者准备好可以写,它就会尽可能地完成它的写操作。
    ·饥饿:一个线程无限期地阻塞,无法进展
    ·线程安全:当且仅当被多个并发线程反复地调用时,它会一直产生正确的结果
    ·线程不安全:如果一个函数不是线程安全的,就是线程不安全的
    ·线程不安全函数类:

    ·不保护共享变量的函数
    ·保持跨越多个调用的状态的函数。
    ·返回指向静态变量的指针的函数。解决办法:重写函数和加锁拷贝。
    ·调用线程不安全函数的函数。
    

    ·可重入函数:当它们被多个线程调用时,不会引用任何共享数据。可重入函数是线程安全函数的一个真子集
    ·显式可重入:没有指针,没有引用静态或全局变量
    ·隐式可重入:允许它们传递指针
    ·可重入性即使调用者也是被调用者的属性,并不只是被调用者单独的属性。
    ·使用线程不安全函数的可重入版本,名字以_r为后缀结尾。
    ·竞争:当一个程序的正确性依赖于一个线程要在另一个线程到达y点之前到达它的控制流中的x点时,就会发生竞争。
    ·线程化的程序必须对任何可行的轨迹线都正确工作。
    ·消除方法:动态的为每个整数ID分配一个独立的块,并且传递给线程例程一个指向这个块的指针
    ·死锁:一组线程被阻塞了,等待一个永远也不会为真的条件
    ·程序员使用P和V操作不当,以至于两个信号量的禁止区域重叠。
    ·重叠的禁止区域引起了一组称为死锁区域的状态。
    ·死锁是不可预测的。
    ·线程与进程总结

    代码调试中的问题及解决

    问题1:运行教材12-15的代码时出现如下问题:

    解决:由于pthread库不是Linux系统默认的库,连接时需要使用库libpthread.a,所以在使用pthread_create创建线程时,要在编译语句末尾加-lpthread参数:

    问题2:教材12-15的代码运行后会出现两种运行结果:

    解决:存在竞争,不确定对等线程在执行第18行之前是否执行了第24行

    问题3:教材12-16的代码运行结果一直是:

    解决:指令错误,添加代码之后运行结果显示正确:

    condvar

    mutex用于保护资源,wait函数用于等待信号,signal函数用于通知信号。其中wait函数中有一次对mutex的释放和重新获取操作,因此生产者和消费者并不会出现死锁。

    #include <stdlib.h>
    #include <pthread.h>
    #include <stdlib.h>
    
    typedef struct _msg{
        struct _msg * next;
        int num;
    } msg;
    
    msg *head;
    pthread_cond_t has_product = PTHREAD_COND_INITIALIZER;
    pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
    void *consumer ( void * p )
    {
        msg * mp;
    
        for( ;; ) {
            pthread_mutex_lock( &lock );  //占用锁
            while ( head == NULL )
                pthread_cond_wait( &has_product, &lock );  //调用cond_wait函数释放锁,并等待condtion通知
            mp = head;
            head = mp->next;
            pthread_mutex_unlock ( &lock );  //释放锁
            printf( "Consume %d tid: %d
    ", mp->num, pthread_self());
            free( mp );
            sleep( rand() % 5 );
        }
    }
    
    void *producer ( void * p )
    {
        msg * mp;
        for ( ;; ) {
            mp = malloc( sizeof(msg) );
            pthread_mutex_lock( &lock );  //占用锁
            mp->next = head;
            mp->num = rand() % 1000;
            head = mp;
            printf( "Produce %d tid: %d
    ", mp->num, pthread_self());
            pthread_mutex_unlock( &lock );  //释放锁
            pthread_cond_signal( &has_product );  //调用cond_signal函数释放资源锁
            sleep ( rand() % 5);
        }
    }
    
    int main(int argc, char *argv[] )
    {
        pthread_t pid1, cid1;
        pthread_t pid2, cid2;
        srand(time(NULL));  //时间种子函数,产生随机数
        pthread_create( &pid1, NULL, producer, NULL);
        pthread_create( &pid2, NULL, producer, NULL);
        pthread_create( &cid1, NULL, consumer, NULL);
        pthread_create( &cid2, NULL, consumer, NULL);  //创建四个新线程
        pthread_join( pid1, NULL );  //pthread_join会阻塞,直到pid1线程终止
        pthread_join( pid2, NULL );  //pthread_join会阻塞,直到pid2线程终止
        pthread_join( cid1, NULL );  //pthread_join会阻塞,直到cid1线程终止
        pthread_join( cid2, NULL );  //pthread_join会阻塞,直到cid1线程终止
        return 0;
    }
    

    程序不断循环

    count

    这是一个不加锁的创建两个线程共享同一变量都实现加一操作的程序,在程序中虽然每个线程都给count加了5000,但由于结果的互相覆盖,最终输出值不是10000,而是5000

    #include <stdio.h>
    #include <stdlib.h>
    #include <pthread.h>
    #define NLOOP 5000
    int counter;
    void *doit( void * );
    int main(int argc, char **argv)
    {
        pthread_t tidA, tidB;
    
        pthread_create( &tidA ,NULL, &doit, NULL );
        pthread_create( &tidB ,NULL, &doit, NULL );  //创建两个新线程
    
        pthread_join( tidA, NULL );  //pthread_join会阻塞,直到tidA线程终止
        pthread_join( tidB, NULL );  //pthread_join会阻塞,直到tidB线程终止
    
        return 0;
    }
    void * doit( void * vptr)
    {
        int i, val;
    
        for ( i=0; i<NLOOP; i++ ) {
            val = counter++;
            printf("%x: %d 
    ", (unsigned int) pthread_self(), val + 1);
            counter = val + 1;
        }
    
    }
    

    countwithmutex

    #include <stdio.h>
    #include <stdlib.h>
    #include <pthread.h>
    
    #define NLOOP 5000
    
    int counter;
    
    pthread_mutex_t counter_mutex = PTHREAD_MUTEX_INITIALIZER;
    
    void *doit( void * );
    
    int main(int argc, char **argv)
    {
        pthread_t tidA, tidB;
    
        pthread_create( &tidA ,NULL, &doit, NULL );
        pthread_create( &tidB ,NULL, &doit, NULL );  //创建两个新线程
    
        pthread_join( tidA, NULL );  //pthread_join会阻塞,直到tidA线程终止
        pthread_join( tidB, NULL );  //pthread_join会阻塞,直到tidB线程终止
    
        return 0;
    }
    
    void * doit( void * vptr)
    {
        int i, val;
    
        for ( i=0; i<NLOOP; i++ ) {
            pthread_mutex_lock( &counter_mutex );  //占用互斥锁
            val = counter++;
            printf("%x: %d 
    ", (unsigned int) pthread_self(), val + 1);
            counter = val + 1;
            pthread_mutex_unlock( &counter_mutex );  //互斥锁解锁
        }
        return NULL;
    }
    

    cp_t

    ·用法:./cp_t [源文件名] [目的文件名] [创建线程数]

    createthread

    ·这个程序作用是打印进程和线程ID,当创建线程成功时,函数返回0,若不为0则说明创建线程失败,常见的错误返回代码为EAGAIN和EINVAL

    semphore

    #include <stdio.h>
    #include <pthread.h>
    #include <stdlib.h>
    #include <semaphore.h>
    
    #define NUM 5
    int queue[NUM];
    sem_t blank_number, product_number;
    
    void *producer ( void * arg )
    {
        static int p = 0;
    
        for ( ;; ) {
            sem_wait( &blank_number );  //调用sem_wait()可以获得资源,使semaphore的值减1,如果调用sem_wait()时semaphore的值已经是0,则挂起等待
            queue[p] = rand() % 1000;
            printf("Product %d 
    ", queue[p]);
            p = (p+1) % NUM;
            sleep ( rand() % 5);
            sem_post( &product_number );  //调用sem_post()释放资源,使semaphore的值加1,同时唤醒挂起等待的线程
        }
    }
    void *consumer ( void * arg )
    {
    
        static int c = 0;
        for( ;; ) {
            sem_wait( &product_number );  ////调用sem_wait()可以获得资源,使semaphore的值减1,如果调用sem_wait()时semaphore的值已经是0,则挂起等待
            printf("Consume %d
    ", queue[c]);
            c = (c+1) % NUM;
            sleep( rand() % 5 );
            sem_post( &blank_number );  //调用sem_post()释放资源,使semaphore的值加1,同时唤醒挂起等待的线程
        }
    }
    
    int main(int argc, char *argv[] )
    {
        pthread_t pid, cid;
        
        sem_init( &blank_number, 0, NUM );
        sem_init( &product_number, 0, 0);
        pthread_create( &pid, NULL, producer, NULL);
        pthread_create( &cid, NULL, consumer, NULL);
        pthread_join( pid, NULL );
        pthread_join( cid, NULL );
        sem_destroy( &blank_number );
        sem_destroy( &product_number );
        return 0;
    }
    

    运行结果:

    share

    ·该程序可获得线程的终止状态

    #include <stdio.h>
    #include <stdlib.h>
    #include <pthread.h>
    #include <unistd.h>
    char buf[BUFSIZ];
    
    void *thr_fn1( void *arg )
    {
        printf("thread 1 returning %d
    ", getpid());
        printf("pwd:%s
    ", getcwd(buf, BUFSIZ));
        *(int *)arg = 11;
        return (void *) 1;  //从线程函数return终止线程
    }
    
    void *thr_fn2( void *arg )
    {
        printf("thread 2 returning %d
    ", getpid());
        printf("pwd:%s
    ", getcwd(buf, BUFSIZ));
        pthread_exit( (void *) 2 );  调用pthread_exit终止线程
    }
    
    void *thr_fn3( void *arg )
    {
        while( 1 ){
            printf("thread 3 writing %d
    ", getpid());
            printf("pwd:%s
    ", getcwd(buf, BUFSIZ));
            sleep( 1 );
        }
    }
    int n = 0;
    
    int main( void )
    {
        pthread_t tid;
        void *tret;
    
        pthread_create( &tid, NULL, thr_fn1, &n);
        pthread_join( tid, &tret );
        printf("n= %d
    ",  n );
        printf("thread 1 exit code %d
    ", (int) tret );
    
        pthread_create( &tid, NULL, thr_fn2, NULL);
        pthread_join( tid, &tret );
        printf("thread 2 exit code %d
    ", (int) tret );
        
        pthread_create( &tid, NULL, thr_fn3, NULL);
        sleep( 3 );
        pthread_cancel(tid);  //调用pthread_cancel终止同一进程中的另一个线程
        pthread_join( tid, &tret );
        printf("thread 3 exit code %d
    ", (int) tret );
        
    }
    

    ·运行结果:

    threadexit

    hello_multi

    hello_multi1

     #include  <stdio.h>
     #include  <pthread.h>
     #define    NUM 5
    void    *print_msg(void *);
    int main()
    {
        pthread_t t[100];       
        char arg[30];
        int i;
        for(i=0; i<100; i++){
            sprintf(arg, "hello%d
    ", i);
            pthread_create(&t[i], NULL, print_msg, (void *)arg);  //创建新线程t[i]
        }
    
        for(i=0; i<100; i++)
            pthread_join(t[i], NULL);  //等待线程t[i]结束 
    
        return 0;
    }
    
    void *print_msg(void *m)
    {
        char *cp = (char *) m;
        int i;
        for(i=0 ; i<NUM ; i++){
            printf("%s", m);
            fflush(stdout);
            sleep(1);
        }
        return NULL;
    }
    

    ·运行结果

    hello_single

    每隔一秒在屏幕上打印一个hello,总共打印5个hello一个world后换行,再打印四个world并且每个都换行

    incprint

    twordcount1&twordcount2

    ·用法:./twordcount1 [文件1] [文件2]
    ·用来统计文件1及文件2两个文件的总字数
    ·运行结果:
    twordcount1:

    twordcount2:

    twordcount3

    ·用法:./twordcount3 [文件1] [文件2]
    ·分别对文件1、文件2两个文件进行字数统计,并把两个文件的字数加起来统计两个文件的总字数
    ·运行结果:

    twordcount4

    ·用法:./twordcount4 [文件1] [文件2]
    ·分别运行代码,根据锁的情况分别统计字数,再把两个文件的字数加起来统计两个文件的总字数
    ·运行结果:

    代码托管

    ·代码托管链接

    ·代码行数统计:

    学习进度条

    代码行数(新增/累积) 博客量(新增/累积) 学习时间(新增/累积)
    目标 5000行 30篇 400小时
    第零周 0/0 1/1 10/10
    第一周 100/100 1/2 20/20
    第二周 120/220 1/3 35/55
    第三周 226/446 1/4 30/85
    第五周 141/587 1/5 30/115
    第六周 150/737 1/6 25/140
    第七周 100/837 1/7 20/160
    第八周 0/837 2/9 30/190
    第九周 183/1020 2/11 20/210
    第十周 521/1541 3/14 20/230
    第十一周 426/1967 1/15 35/265
    第十二周 41/2008 3/18 50/315
    第十三周 1281/2008 1/18 40/355
  • 相关阅读:
    常用cmd命令总结
    百度地图api的简单应用(二):轻量级路径规划
    百度地图api的简单应用(一):POI检索
    R语言-八皇后问题
    8086基本IO口的仿真
    输入输出与中断
    汇编语言例子
    变量标号
    变量声明
    串操作指令2
  • 原文地址:https://www.cnblogs.com/20145210ysy/p/6159432.html
Copyright © 2011-2022 走看看