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

一、学习目标

二、学习内容

并发程序

  • 并发程序:使用应用级并发的应用程序。

【重点!!!】现代操作系统提供了三种基本的构造并发程序的方法:

(1)进程

  属于程序级并发。每个逻辑控制流都是一个进程,由内核来调度和维护。由于进程有独立的虚拟地址空间,需要使用显式的进程间通信机制(IPC)来与其他流通信。

(2)I/O多路复用

  这种形式的并发编程中,应用程序在一个进程的上下文中显式地调度它们的逻辑流。逻辑流被模型化为状态机,数据到达文件描述符后,主程序显式地从一个状态转换为另一个状态。由于程序时一个单独的进程,因此所有的I/O多路复用流共享同一个地址空间。

(3)线程

  属于函数级并发。线程是运行在一个单一进程上下文中的逻辑流,由内核进行调度。线程流像进程流一样由内核进行调度,像I/O多路复用流一样共享着同一个虚拟地址空间。

三种并发编程

1. 基于进程的并发编程

构造并发编程最简单的方法就是用进程构造,使用fork、exec和waitpid等函数。

  • 构造模型图

  • 构造步骤
(1)服务器监听一个监听描述符上的连接请求。
(2)服务器接受了客户端1的连接请求,并返回一个已连接描述符。
(3)在接受了连接请求之后,服务器派生一个子进程,这个子进程获得服务器描述符表的完整拷贝。子进程关闭它的拷贝中的监听描述符3,而父进程关闭它的已连接描述符4的拷贝,因为不再需要这些描述符了。
(4)子进程正忙于为客户端提供服务,父进程继续监听新的请求。

注意:子进程关闭监听描述符和父进程关闭已连接描述符是很重要的,因为父子进程共用同一文件表,文件表中的引用计数会增加,只有当引用计数减为0时,文件描述符才会真正关闭。所以,如果父子进程不关闭不用的描述符,将永远不会释放这些描述符,最终将引起存储器泄漏而最终消耗尽可以的存储器,是系统崩溃。

  • 注意事项
(1)通常服务器会运行很长的时间,所以我们必须要包括一个SIGCHLD处理程序,来回收僵死子进程的资源。因为当SIGCHLD处理程序执行时,SIGCHLD信号时阻塞的,而Unix信号时不排队的,所以SIGCHLD处理程序必须准备好回收多个僵死子进程的资源。
(2)其次,子进程必须关闭它们各自的connfd拷贝。就像我们已经提到过的,这对父进程而言尤为重要,它必须关闭它的已连接描述符,以避免存储器泄漏。
(3)最后,因为套接字的文件表表项中的引用计数,直到父子进程的connfd都关闭了,到客户端的连接才会终止。
  • 进程的优劣
优点:
(1)共享文件表,但是不共享用户地址空间。
(2)进程有独立的地址空间使得进程不可能覆盖另一个进程的虚拟存储器,消除迷惑。
缺点:
(1)进程有独立的地址空间也使得进程共享状态信息变得更加困难。为了共享信息,它们必须使用显式的IPC(进程间通信)机制。
(2)基于进程的设计往往比较慢,因为进程控制和IPC的开销很高。

2. 基于I/O多路复用的并发编程

  • I/O多路复用解决服务器必须响应两个互相独立的I/O事件的困境
(1)网络客户端发起的连接请求
(2)用户在键盘上键入的命令。

基本思想是,使用select函数,要求内核挂起进程,只有在一个或多个I/O事件发生后,才将控制返回给应用程序。
可以使用select、poll和epoll来实现I/O多路复用的并发编程。

  • I/O多路复用技术的优劣
优点:
(1)使用事件驱动编程比基于进程的设计给了程序更多的对程序行为的控制。
(2)一个基于I/O多路复用的事件驱动服务器是运行在单一进程上下文中的,因此每个逻辑流都访问该进程的全部地址空间。这使得在流之间共享数据变得很容易。
缺点:
(1)编码复杂;
(2)不能充分利用多核处理器。

3. 基于线程的并发编程

在使用进程并发编程中,我们为每个流使用了单独的进程。内核会自动调用每个进程。每个进程有它自己的私有地址空间,这使得流共享数据很困难。在使用I/O多路复用的并发编程中,我们创建了自己的逻辑流,并利用I/O多路复用来显式地调度流。因为只有一个进程,所有的流共享整个地址空间。而基于线程的方法,是这两种方法的混合。

  • 线程执行的模型图

  多线程的执行模型在某些方面和多进程的执行模型是相似的。每个进程开始生命周期时都是单一线程,这个线程是主线程。在某一时刻,主线程创建一个对等线程,从这个时间点开始,两个线程就并发地运行。最后,因为主线程执行一个慢速系统调用,例如read和sleep,或者因为它被系统的间隔计时器中断,控制就会通过上下文切换到对等线程。对等线程会执行一段时间,然后控制传递回主线程,依次类推。

  线程执行时有时也不同于进程。因为一个线程的上下文要比一个进程的上下文小很多,线程的上下文切换要比进程的上下文切换快得多。另一个不同就是线程不像进程那样,不是按照严格的父子层次来组织的。和一个进程相关的线程组成一个对等(线程)池,独立于其他线程创建的线程。主线程和其他线程的区别仅在于它总是进程中第一个运行的线程。对等(线程)池概念的主要影响是一个线程可以杀死它的任何对等线程,或者等待它的任意对等线程终止。另外每个对等线程都能读写相同的共享数据。

【重点!!!】基于线程的并发编程实现(系统调用)

(1)创建线程

//线程通过调用pthread_create函数来创建其他线程:

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

//成功返回0,失败返回非零

(2)初始化线程

//该函数用来初始化多个线程共享的全局变量

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

//总是返回0

(3)获取线程ID

//pthread_create函数创建一个新的线程,并带着一个输入变量arg,在新线程的上下文中运行线程例程f。能用attr参数来改变新创建线程的默认属性。当pthread_create返回时,参数tid包含新创建线程的ID。新线程可以通过调用pthread_self函数来获得它自己的线程ID。

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

//返回调用者的线程PID

(4)终止线程

 一个线程是以下列方式之一来终止的:

  • 当顶层的线程例程返回时,线程会隐式地终止
  • 通过调用pthread_exit函数,线程会显式地终止。如果主线程调用pthread_exit,它会等待所有其他对等线程终止,然后再终止主线程和这个进程,返回值为thread_return。
  • 某个对等线程调用exit函数,则函数终止进程和所有与该进程相关的线程;
  • 另一个对等线程调用以当前ID为参数的函数ptherad_cancel来终止当前线程。
//调用pthread_exit显示地终止
 #include <pthread.h>
 void pthread_exit(void *thread_return);
//成功返回0,失败返回非零

//以当前线程ID为参数调用pthread_cancel函数终止当前线程
 #include <pthread.h>
 int pthread_cancel(pthread_t tid);
//成功返回0,失败返回非零

(5)回收已终止线程的资源

//pthread_join函数会终止,直到线程tid终止,将线程例程返回的(void*)指针赋值为thread_return指向的位置,然后回收已终止线程占用的所有存储器资源。和wait不同,该函数只能回收指定id的线程,不能回收任意线程。

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

//成功返回0,失败返回非零

(6)分离线程

  • 在任何一个时间点上,线程是可结合的或者是分离的。一个可结合的线程能够被其他线程收回其资源和杀死。在被其他线程回收之前,它的存储器资源(例如栈)式没有被释放的。相反,一个分离的线程是不能被其他线程回收和杀死的。它的存储器资源在它终止时由系统自动释放。

  • 默认情况下,线程被创建成可结合的。为了避免存储器泄漏,每个可结合线程都应该要么被其他线程显式地收回,要么通过调用pthread_detach函数被分离。

//pthread_detach函数分离可结合线程tid。线程能够通过以pthread_self()为参数的pthread_detach调用来分离它们自己。

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

//总是返回0

【重点!!!】线程同步互斥及相关系统调用

1. 概念理解

(1)互斥与同步

  当线程并发执行时,由于资源共享和线程协作,使用线程之间会存在互斥与同步两种制约关系。

  • 互斥:间接相互制约。一个系统中的多个线程必然要共享某种系统资源,一个线程在执行时,其它线程都要等待。互斥其实是一种特殊的同步。
  • 同步:直接相互制约。这种制约主要是因为线程之间的合作,如有线程A将计算结果提供给线程B作进一步处理,那么线程B在线程A将数据送达之前都将处于阻塞状态。

(2)互斥锁

  由于线程共享进程的资源和地址空间,所以在访问到他们的公共资源的时候,一定会出现线程的同步和互斥现象,多线程访问一个数据的次序一定是杂乱无章的,这个时候引入互斥锁的概念。

  • 互斥锁,用于保证在某一段时间只有一个线程在执行某些代码。

  • 互斥锁的实现

函数 作用
pthread_mutex_t mutex; 定义锁
pthread_mutex_init(&mutex, NULL); 默认属性初始化锁
pthread_mutex_lock(&mutex); 申请锁
pthread_mutex_unlock(&mutex); 释放锁

2. 实现互斥与同步需要用到的系统调用

(1)互斥

#include <pthread.h>
int pthread_mutex_destory(pthread_mutex_t *mutex);
int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

(2)同步

#include <pthread.h>
int pthread_cond_destroy(pthread_cond_t *cond);
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

(3)互斥锁

#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
pthread_mutex_t unlock(pthread_mutex_t *mutex);

3. 基于线程的编程的linux gcc编译

  • gcc XXX.c -lpthread -o XXXX
  • gcc XXX.c -pthread -o XXXX
原文地址:https://www.cnblogs.com/moddy13162201/p/10016481.html