2017-2018-1 20155339平措卓玛 《信息安全系统设计基础》第十三周学习总结
2017-2018-1 20155339 《信息安全系统设计基础》第十三周学习总结
教材学习内容总结
什么是并发?
- 答:如果逻辑控制流在时间上重叠,那么他们就是并发的。并发出现在计算机系统的很多不同的层面上。我们主要将并发看成是一种操作系统内核用来运行多个应用程序的机制。
并发不仅仅局限于内核,它也可以在应用程序中扮演重要的角色,那么应用级并发在哪些情况下也可以发挥其重要作用呢?
-
1.访问慢速I/O设备:即当一个应用正在等待来自慢速I/O设备(如磁盘)的数据到达时,内核会先运行其他程序。
-
2.与人交互:效果为使用者可以同时使用的计算机的多个功能。
-
3.通过推迟工作以降低延迟:应用程序推迟某项操作或者并发的执行一些操作,利用并发来降低一些延迟。
-
4.服务多个网络客户端:一个服务器允许多个客户端同时连接。
-
5.在多核机器上进行并行计算:多核处理器中包含多个CPU。
使用应用级并发的应用程序称为并发程序,现代操作系统提供了三种基本的构造并发程序的方法,是哪三种呢?
-
进程:每个逻辑控制流都是一个进程,由内核来调度和维护。控制流使用显式的进程间通信(IPC)机制。
-
I/O多路复用:应用程序在一个进程的上下文中显式地调度他们自己的逻辑流。所有的流都共享同一个地址空间。
-
线程:线程是运行在一个单一进程上下文中的逻辑流,由内核进行调度。
基于进程的并发编程
基于进程的并发编程可以有哪些方法?
- 构造并发程序最简单的方法就是用进程。可以使用一些函数,例如fork、exec和waitpid等,一个自然地构造报并发程序的方法是在父进程中接受客户端的连接请求,然后创建一个子进程为每个客户端提供服务。
怎么理解上面所描述的方法?
- 其实就是父进程在不断监听,一旦有客户端发送连接请求,服务器便生成子进程1来为其服务,此时必须关闭父进程的已连接描述符,以及子进程的监听描述符,当再有客户端发来请求时再根据同样的步骤生成子进程2来为其服务,这样服务器就可以同时为多个客户端服务。
需要注意的
- 父子进程必须关闭它们各自的connfd拷贝。父进程必须关闭它的已连接描述符,以避免存储器泄漏。直到父子进程的connfd都关闭了,到客户端的连接才会终止。
- 父、子进程间共享状态信息,进程有一个非常清晰的模型:共享文件表,但是不共享用户地址空间。
- 使用显式的进程间通信(IPC)机制。但开销很高,往往比较慢。
基于I/O多路复用的并发编程
什么时候需要使用I/O多路复用并发编程呢?
- 当我们需要写一个也能对用户从标准输入键入的交互式命令作出响应的服务器时,我们需要使用I/O多路复用并发编程。因为这种时候我们既需要响应客户端发起的链接请求,也需要对用户键入的命令行作出反应。
怎么使用I/O多路复用并发编程
-
I/O 多路复用(I/O multiplexing)技术:基本的思路就是使用select函数,要求内核挂起进程,只有在一个或多个I/O事件发生后,才将控制返回给应用程序。
-
函数原型:
int select(int n,fd_set *fdset,NULL,NULL,NULL); 返回已经准备好的描述符的非0的个数,若出错则为-1。
-
select函数处理类型为fd_set的集合,也叫做描述符集合,我们可以看做一个大小为n位的向量:bn-1,......,b1,b0。
-
对描述符集合的处理方法只能有以下三种:
1.分配他们;
2.将一个此种类型的变量赋值给另一个变量;
3.用FD_ZERO,FD_SET,FD_CLR和FD_ISSET宏指令来修改和检查他们。
详解select函数
-
select函数一直处于阻塞状态,直到由用户在键盘输入并且敲下回车或者有客户端发来连接请求才会调用不同的函数来执行响应,如果是用户在键盘输入并且敲下回车则调用command函数,如果是客户端发来连接请求则调用accept函数来执行响应。
-
但是存在一个问题就是一旦被一个客户端占用,标准输入就很久不会得到响应,解决的方法是更细粒度的多路复用。
基于I/O多路复用的并发事件驱动服务器
-
I/O多路复用可以用作事件并发驱动程序的基础。
-
将逻辑流模型化为状态机。一个状态机从某种初始状态开始执行。每个输入事件都会引发一个从当前状态到下一状态的转移。
I/O 多路复用技术有什么优劣
-
优点:比基于进程的设计给了程序员更多的对进程行为的控。一个基于 I/O多路复用的事件驱动服务器是运行在单一进程上下文中的,因此每个逻辑流都能访问该进程的全部地址空间。
-
缺点:编码复杂。
基于线程的并发编程
- 线程是以上两种方法的混合。
什么是线程?
- 线程(thread) :运行在进程上下文中的逻辑流。
- 由内核自动调度,每个进程都有自己的线程上下文,包括一个唯一的整数线程ID,栈、栈指针、程序计数器、通用目的寄存器和条件码。
线程执行模型
-
每个进程开始生命周期时都是单一线程,称为主线程,在某一时刻创建一个对等线程,从此开始并发地运行,最后,因为主线程执行一个慢速系统调用,或者被中断,控制就会通过上下文切换传递到对等线程。
-
线程上下文切换,要比进程的上下文切换快得多,一个线程可以杀死或者等待他的任意对等线程。
c程序中如何处理线程
- 使用Posix 线程,Posix线程是C语言中处理线程的一个标准接口,Pthreads定义了大约69个函数,允许程序创建、杀死和回收线程,与对等线程安全的共享数据,还可以通知对等线程系统状态的变化。
创建线程
-
线程通过调用pthread_create来创建其他线程。
-
函数原型:
int pthread_create(pthread_t *tid,pthread_attr_t *attr,func *f,void *arg); 成功则返回0,出错则为非零
-
pthread_ create 函数创建一个新的线程,并带着一个输入变量arg,在新线程的上下文中运行线程例程f。用attr参数来改变新创建线程的默认属性,我们可以将其设为NULL,返回时,参数tid包含新创建线程的ID。新线程可以通过调用 pthreadself 函数来获得它自己的线程 ID:
pthread_t pthread_self(void);返回调用者的线程ID。
终止线程
- 一个线程是通过以下方式之一来终止的:
1.顶层的线程例程返回时,线程会隐式地终止。
2.通过调用 pthreadexit 函数,线程会显式地终止。如果主线程调用 pthreadexit , 它会等待所有其他对等线程终止,然后再终止主线程和整个进程,返回值为 thread_return: void pthread_exit(void *thread_return);
3.某个对等线程调用exit函数,该函数终止进程以及所有与该进程先关的对等线程。
4.另一个对等线程通过以当前线程ID作为参数调用pthread_cancle函数来终止当前线程。
回收已终止线程的资源
-
线程通过调用 pthread_join 函数等待其他线程终止。
-
函数原型:
int pthread_join(pthread_t tid,void **thread_return); 成功则返回0,出错则为非零
-
pthread _join函数会阻塞,直到线程tid终止,回收已终止线程占用的所有内存资源。
-
与wait函数不同,pthread_join函数只能等待一个指定的线程终止。
分离线程
-
在任何一个时间点上,线程是可结合或可分离的。一个可结合的线程能够被其他线程收回其资源和杀死,在被回收之前,它的内存资源是没有被释放的。相反,分离的线程不能被其他回收或杀死的,它的内存资源在其终止时自动释放。
-
默认情况下,线程被创建成可结合的,为了避免内存泄漏,每个可结合的线程都应该要么被其他进程显式的回收,要么通过调用pthread _detach函数被分离。
-
函数原型:
int pthread_deacth(pthread_t tid); 成功则返回0,出错则为非零
初始化线程
-
pthread_once允许初始化与线程例程相关的状态。
-
函数原型:
pthread_once_t once_control=PTHREAD_ONCE_INIT; int pthread_once(pthread_once_t *once_control,void (*init_routine)(void)); 总是返回0
-
once_control是一个全局或者静态变量,总是被初始化为PTHREAD_ONCE_INIT。
多线程程序中的共享变量
-
一个变量是共享的,当且仅当多个线程引用这个变量的某个实例。
-
寄存器是从不共享的,但是虚拟内存总是共享的。
将变量映射到内存
-
全局变量:定义在函数之外的变量,虚拟存储器的读/写区域只会包含每个全局变量的一个实例,任何线程都可以引用。
-
本地自动变量:定义在函数内部但没有static属性的变量。
-
本地静态变量:定义在函数内部并有static属性的变量。
我们说一个变量v是共享的,当且仅当它的一个实例被一个以上的线程引用。
用信号量同步线程
- 共享变量是十分方便,但是它们也引入了同步错误上网可能性
进度图
-
进度图是将n个并发线程的执行模型化为一条n维笛卡尔空间中的轨迹线,原点对应于没有任何线程完成一条指令的初始状态。
-
进度条将指令模型化为从一种状态到另一种状态的转换。
-
合法的转换规则
1.合法的转换是向右或者向上。
2.两条指令不能在同一时刻完成。
3.不允许向下或者向左移动的转换。 -
互斥:确保每个进程在执行临界区的中的指令时,拥有共享变量的互斥访问。
信号量
-
P(s):如果s是非零的,那么P将s减一,并且立即返回。如果s为零,那么就挂起这个线程,直到s变为非零。一个V操作会重启这个线程。
-
V(s):将s加一,如果有任何线程阻塞在P操作等待s变为非零,那么V操作会重启线程中的一个,然后该线程将s减一,完成他的P操作。
-
v的定义中没有定义等待线程被重启的顺序,唯一的要求是只能重启一个正在等待的线程,因此,不能预测V要重启哪一个线程。
-
sem_init函数将信号量初始化为value,每个信号量在使用前必须初始化,函数原型:
int sem_init(sem_t *sem,0,unsigned int value);//将信号量初始化为value
-
执行P和V操作:
int sem_wait(sem_t *s);//P(s)
int sem_post(sem_t *s);//V(s)
使用信号量来实现互斥
-
信号量提供了一种很方便的方法来确保对共享变量的互斥访问:将每个共享变量(或者一组相关的共享变量)与一个信号量s(初始为1)联系起来,然后用P(s)和V(s)操作将相应的临界区包围起来。
-
禁止区:s<0,因为信号量的不变性,没有实际可行的轨迹线能够包含禁止区中的状态。
读者—写者问题
-
读者优先,要求不让读者等待,除非已经把使用对象的权限赋予了一个写者。
-
写者优先,要求一旦一个写者准备好可以写,它就会尽可能地完成它的写操作。
其他并发问题
线程安全
-
一个线程是安全的,当且仅当被多个并发线程反复的调用时,它会一直产生正确的结果。
-
四个不相交的线程不安全函数类:
1.不保护共享变量的函数。
2.保持跨越多个调用的状态的函数。
3.返回指向静态变量的指针的函数。
4.调用线程不安全函数的函数 -
将上述线程不安全函数编程线程安全的方法分别如下:
1.用P和V这样的同步操作保护共享变量。
2.重写,不用任何static数据。
3.重写或者使用加锁-拷贝技术。
4.可以参考前三种。
可重入性
-
有一类重要的线程安全函数,叫做可重入函数,当它们被多个线程调用时,不会引用任何共享数据。
-
显式可重入的:所有函数参数都是传值传递,没有指针,并且所有的数据引用都是本地的自动栈变量,没有引用静态或全剧变量。
-
隐式可重入的:调用线程小心的传递指向非共享数据的指针。
在线程化的程序中使用已存在的库函数
- Linux系统提供绝大部分线程不安全函数的可重入版本,名字以_r为后缀结尾。
竞争
-
竞争发生的原因:当一个程序的正确性依赖于一个线程要在另一个线程到达y点之前到达它的控制流中的x点。也就是说,程序员假定线程会按照某种特殊的轨迹穿过执行状态空间,忘了一条准则规定:线程化的程序必须对任何可行的轨迹线都正确工作。
-
消除竞争的方法:我们可以动态地为每个整数ID分配一个独立的块,并且传递给线程例程一个指向这个块的指针。
死锁
-
一组线程被阻塞了,等待一个永远也不会为真的条件。
-
死锁是不可预测的。
-
互斥锁加锁顺序规则:如果对于程序中每对互斥锁(s,t),给所有的锁分配一个全序,每个线程按照这个顺序来请求锁,并且按照逆序来释放,这个程序就是无死锁的。
课后练习题
12.1 在下图中,并发服务器的第33行上,父进程关闭了已连接描述符后,子进程仍能够使用该描述符和客户端通信,为什么。
- 答:当父进程派生子进程时,它得到一个已连接描述符的副本,并将相关文件表中的引用计数从1增加到2.当父进程关闭它的描述符副本时,引用计数就从2减少到1.因为内核不会关闭一个文件,知道文件表中它的引用计数值变为0,所以子进程这边的连接端将保持打开。
12.2 如果我们要删除图12-5中关闭已连接描述符的第30行,从没有内存泄漏的角度来说,代码将仍然是正确的,为什么?
- 答:当一个进程因为某种原因终止时,内核将关闭所有打开的描述符。因此,当子进程退出时,它的已连接文件描述符的副本也将被自动关闭。
12.3 在Linux系统里,在标准输入上键入Ctrl+D表示EOF。图12-6中的程序阻塞在对select的调用上,如果你键入Ctrl+D会发生什么
- 答:如果一个从描述符中读一个字节的请求不会阻塞,那么这个描述符就准备好可以读了。假如EOF在一个描述符上为真,那么描述符也准备好可读了,因为读操作将立即返回一个零返回码,表示EOF。因此,键入Ctrl+D会导致select函数返回,准备好的集合中有描述符0.
12.4 图12-8所示的服务器中,我们在每次调用select之前都立即小心地重新初始化pool.ready_set变量,为什么?
-
答:因为变量pool.read_set既作为输入参数,也作为输出参数,所以我们在每一次调用select之前都重新初始化它。在输入时,它包含读集合。在输出时,它包含准备好的集合。
-
12.5 在下图1中基于进程的服务器中,我们在两个位置小心地关闭了已连接描述符:父进程和子进程。然而,在图2中,基于线程的服务器中,我们只在一个位置关闭了已连接描述符:对等线程,为什么?
- 答:因为线程运行在同一个进程中,它们都共享相同的描述符表。无论有多少线程使用这个已连接描述符,这个已连接描述符的文件表的引用计数都等于1.因此,当我们用完它时,一个close操作就足以释放于这个已连接描述符相关的内存资源了。
12.6 根据下图所示代码,完成下表,其中符号v.t表示变量v的第t个实例,m是主线程,p0对等线程0,p1对等线程1,并且指出ptr,cnt,i,msgs,myid哪些是共享变量。
- 答案如下图所示:
12.7 根据badcnt.c的指令顺序完成下表
参考下图所示步骤完成了本表格
- 答:如下图:
12.8 指出下列轨迹是否安全。
A.H1,L1,U1,S1,H2,L2,U2,S2,T2,T1
B.H2,L2,H1,L1,U1,S1,T1,U2,S2,T2
C.H1,H2,L2,U2,S2,L1,U1,S1,T1,T2
- 答:A、C安全,B不安全。轨迹如下图所示
12.9 见下图
12.10下图所示的对第一类读者-写者问题的解答给予读者较高的优先级,但是从某种意义上说,这种优先级是很弱的,因为一个离开临界区的写者可能重启一个在等待的写者,而不是一个在等待的读者。描述一个场景,其中这种弱优先级会导致一群写者使得一个读者饥饿。
- 答:假设一个特殊的信号量实现为每一个信号量使用了一个LIFO的线程栈。当一个线程在P操作中阻塞在一个信号量上,它的ID就被压入栈中。类似地,V操作从栈中弹出栈顶的线程ID,并重启这个线程。根据这个栈的实现,一个在它的临界区中竞争的写者会简单的等待,直到在他释放这个信号量之前另一个写者阻塞在这个信号量上。在这种场景中,当两个写者来回地传递控制权时,正在等待的读者可能会永远的等待下去。
12.11 见下图
12.12 图12-38中的ctime_ts函数是线程安全的,但不是可重入的,请解释说明。
- 答:ctime_ts函数不是可重入函数,因为每次调用都共享相同的由ctime函数返回的static变量。然而,它是线程安全的,因为对共享变量的访问是被P和V操作保护的,因此是互斥的。
12.13 在下图所示代码中,我们可能想要在主线程中的第14行后立即释放已经分配的内存块,而不是在对等线程中释放它,这是个坏主意,为什么?
- 答:如果在第4行刚调用完pthread_create后就释放内存,这回引起一个竞争,这个竞争发生在主线程对free的调用和24的行的赋值语句之间。
12.14
A.在下图中,我们通过为每个整数ID分配一个独立的块来消除竞争。给出一个不调用malloc或者free函数的不同的方法。
B.这种方法的利弊是什么?
- 答:A.另一种方法是直接传递整数i,而不是传递一个指向i的指针:
for(i=0;i<N;i++)
Pthread_create(&tid[i],NULL,thread,(void*)i);
在线程例程中,我们将参数强制转换成一个int型变量,并将它赋值给myid;
int myid=(int)vargp;
B.优点是它通过消除对malloc和free的调用降低了开销。一个明显的缺点是,它假设指针至少和int一样大。即便这种假设对于所有得现代系统来说都为真,但是它对于那些过去遗留下来的或今后的系统来说可能就不为真了。
家庭作业
12.16 编写一个hello.c,他创建和回收n个可结合的对等线程,其中n是一个命令行参数。
- 代码如下:
#include<stdio.h>
#include<stdlib.h>
#include<fcntl.h>
#include<unistd.h>
#include<pthread.h>
void *thread(void *vargp);
int main(int argc, char **argv)
{
pthread_t tid;
int i,n;
n = atoi(argv[1]);
for(i=0;i<n;i++)
{
pthread_create(&tid,NULL,thread,NULL);
pthread_join(tid,NULL);
}
exit(0);
}
void *thread(void *vargp)
{
printf("hello world!\n");
return NULL;
}
- 运行结果如下
12.17 运行如下代码 A.并不会输出字符,是为什么?
B.通过是用什么函数来改正这个错误呢?
#include<stdio.h>
#include<stdlib.h>
#include<fcntl.h>
#include<unistd.h>
#include<pthread.h>
void *thread(void *vargp);
int main(int argc, char **argv)
{
pthread_t tid;
int i,n;
pthread_create(&tid,NULL,thread,NULL);
exit(0);
}
void *thread(void *vargp)
{
sleep(1);
printf("hello world!\n");
return NULL;
}
- 答:A.是因为exit(0),使得线程在主线程中结束了,所以没有打印出字符。
B.使用pthread_join就可以解决这个问题,如下图:
12.18 用进度图说明下面的轨迹分类是否安全:
A.H2,L2,U2,H1,L1,S2,U1,S1,T1,T2
B.H2,H1,L1,U1,S1,L2,T1,U2,S2,T2
C.H1,L1,H2,L2,U2,S2,U1,S1,T1,T2
- 答:结果:A不安全,B安全,C安全,过程如下图
12.19 教材p707给出的代码,给予了读者比较弱的优先级,当读者离开缓冲区时,可能会重启一个正在等待的写着,而不是一个正在等待的读者,试着更改代码,使得读者优先。
- 信号量代码如下:
int readcnt;
sem_t mutex=1,w=1,z=1;
void reader(void)
{
while(1)
{
P(&mutex);
readcnt++;
if(readcnt==1)
P(&w);
V(&mutex);
P(&mutex);
readcnt--;
if(readcnt==0)
V(&w);
V(&mutex);
}
}
void writer(void)
{
while(1)
{
P(&z);
P(&w);
V(&w);
V(&z);
}
}
12.22 检查对select函数的理解,修改下图所示服务器使得它在主服务器每次迭代中至多只会送一个文本:
答:在while循环中增加一个FD_AERO(&ready_set);
就可以了。
12.24 RIO I/O包中的函数都是线程安全的,那么他们都是是可重入的吗?
- 答:是不可重入的,RIO包中有专门的数据结构为每一个文件描述符都分配了相应的独立的读缓冲区,它提供了与系统I/O类似的函数接口,在读取操作时,RIO包加入了读缓冲区,一定程度上增加了程序的读取效率。另外,带缓冲的输入函数是线程安全的,这与Stevens的 UNP 3rd Edition(中文版) P74 中介绍的那个输入函数不同。UNP的那个版本的带缓冲的输入函数的缓冲区是以静态全局变量存在,所以对于多线程来说是不可重入的。
12.25 些图所示代码中echo_cnt函数是线程安全的吗?是可重入的吗?
- 答:是可重入的,因为,其中的参数connfd是局部非静态变量,那么对于每一个线程,他都可以是不同的值,所以是安全的并且是可重入的。
12.29 下面的程序会死锁吗?为什么?
初始时:a=1;b=1;c=1
线程1 : 线程2:
p(a); p(c);
p(b); p(b);
v(b); v(b);
p(c); v(c);
v(c);
v(a);
答: 会死锁,因为线程1和2在执行完第一步之后都被挂起,都得不到需要的资源。
代码练习
练习一进程的并发编程
- 客户端代码:
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <netdb.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
int main(int argc, char **argv){
int port,clientfd;
struct hostent *hp;
struct sockaddr_in serveraddr;
port = atoi(argv[2]);
if((hp = gethostbyname(argv[1])) == NULL){ //通过ip地址获取目的主机信息
printf("ip error\n");
}
if((clientfd = socket(AF_INET, SOCK_STREAM, 0)) < 0){ //创建客户端套接字
printf("socket error\n");
}
bcopy((char *)hp->h_addr_list[0], (char *)&serveraddr.sin_addr.s_addr, hp->h_length);
serveraddr.sin_family = AF_INET;
serveraddr.sin_port = htons(port);
if(connect(clientfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)) < 0){//创建连接
printf("connect error\n");
}else{
printf("connect successful!\n");
while(1){
char buf[100];
char get[100];
read(clientfd, get, 100);
if(strcmp(get, "quit") == 0){
printf("server exit\n");
close(clientfd);
return 0;
}
printf("%s \n", get);
scanf("%s", buf);
write(clientfd, buf, 100);
}
}
return 0;
}
- 多进程服务器代码
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
int echo(int connfd){
write(connfd, "Please input", 100);
while(1){
char buf[100];
read(connfd, buf, 100);
printf("%s \n", buf);
printf("receive:");
write(connfd, buf, 100);
if(strcmp(buf, "quit") == 0){
printf("client exit\n");
return 1;
}else{
continue;
}
}
}
int main(int argc, char **argv){
int port, listenfd, connfd, clientlen;
struct sockaddr_in clientadder;
struct hostent *hp;
port = atoi(argv[1]);
if((listenfd = socket(AF_INET, SOCK_STREAM, 0)) < 0){
printf( "socket error\n" );
}
clientadder.sin_family = AF_INET;
clientadder.sin_addr.s_addr = htonl(INADDR_ANY);
clientadder.sin_port = htons(port);
if(bind(listenfd, (struct sockaddr *)&clientadder, sizeof(clientadder)) < 0){
printf( "bind error\n" );
}
if (listen(listenfd, 10) < 0){
printf( "listen error\n" );
}
while(1){
printf( "wait for client\n" );
clientlen = sizeof(clientadder);
if((connfd = accept(listenfd, (struct sockaddr *)&clientadder, &clientlen)) < 0){//父进程创建连接描述符
printf( "accept error\n" );
}else{
if(fork() == 0){
close(listenfd);//子进程关闭来自父进程的监听描述符
printf("connect successful!\n");
if (echo(connfd)){
printf("exit from echo\n");
}else{
printf("error from echo\n");
}
write(connfd, "exit", 100);
close(connfd);//关闭父进程送进来的连接描述符
exit(0);
}
close(connfd);//父进程关闭已被子进程复制的连接描述符
}
}
}
- 运行结果
练习二线程的并发编程
-
客户端代码同上。
-
服务器端代码:
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <pthread.h>
int echo(int connfd){
write(connfd, "Please input", 100);
while(1){
char buf[100];
read(connfd, buf, 100);
printf("%s \n", buf);
write(connfd, buf, 100);
if(strcmp(buf, "exit") == 0){
printf("client exit\n");
return 1;
}else{
continue;
}
}
}
void *thread(void *vargp){//线程例程
printf("connect successful!\n");
int connfd2 = *((int *)vargp);
pthread_detach(pthread_self());
if (echo(connfd2)){
printf("exit from echo\n");
}else{
printf("error from echo\n");
}
write(connfd2, "exit", 100);
free((int *)vargp);
close(connfd2);
return NULL;
}
int main(int argc, char **argv){
int port, listenfd, connfd, clientlen;
struct sockaddr_in clientadder;
struct hostent *hp;
port = atoi(argv[1]);
if((listenfd = socket(AF_INET, SOCK_STREAM, 0)) < 0){
printf( "socket error\n" );
}
clientadder.sin_family = AF_INET;
clientadder.sin_addr.s_addr = htonl(INADDR_ANY);
clientadder.sin_port = htons(port);
if(bind(listenfd, (struct sockaddr *)&clientadder, sizeof(clientadder)) < 0){
printf( "bind error\n" );
}
if (listen(listenfd, 10) < 0){
printf( "listen error\n" );
}
while(1){
printf( "wait for client\n" );
clientlen = sizeof(clientadder);
int *connfd = malloc(sizeof(int));
if((*connfd = accept(listenfd, (struct sockaddr *)&clientadder, &clientlen)) < 0){
printf( "accept error\n" );
}else{
pthread_t this_id;
printf("connfd = %d \n",*connfd);
pthread_create(&this_id, NULL, thread, connfd);//创建一个线程,跳向线程例程
}
}
}
- 运行截图
练习三读者优先
- 代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <semaphore.h>
#define N_WRITER 30
#define N_READER 5
#define W_SLEEP 1
#define R_SLEEP 1
pthread_t wid[N_WRITER],rid[N_READER];
pthread_mutex_t mutex_write;
sem_t sem_read;
int data = 0;
int readerCnt = 0;
void write()
{
int rd = rand();
printf("write %d\n",rd);
data = rd;
}
void read()
{
printf("read %d\n",data);
}
void * writer(void * in)
{
pthread_mutex_lock(&mutex_write);
printf("写线程id%d进入数据集\n",pthread_self());
write();
printf("写线程id%d退出数据集\n",pthread_self());
pthread_mutex_unlock(&mutex_write);
sleep(W_SLEEP);
pthread_exit((void *) 0);
}
void * reader (void * in)
{
sem_wait(&sem_read);
readerCnt++;
if(readerCnt == 1){
pthread_mutex_lock(&mutex_write);
}
sem_post(&sem_read);
printf("读线程id%d进入数据集\n",pthread_self());
read();
printf("读线程id%d退出数据集\n",pthread_self());
sem_wait(&sem_read);
readerCnt--;
if(readerCnt == 0){
pthread_mutex_unlock(&mutex_write);
}
sem_post(&sem_read);
sleep(R_SLEEP);
pthread_exit((void *) 0);
}
int main()
{
printf("多线程,读者优先\n");
pthread_mutex_init(&mutex_write,NULL);
sem_init(&sem_read,0,1);
int i = 0;
for(i = 0; i < N_WRITER; i++)
{
pthread_create(&wid[i],NULL,writer,NULL);
}
for(i = 0; i < N_READER; i++)
{
pthread_create(&rid[i],NULL,reader,NULL);
}
sleep(1);
return 0;
}
- 运行结果如下:
写者优先
- 代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include<semaphore.h>
#define N_WRITER 5
#define N_READER 20
#define W_SLEEP 1
#define R_SLEEP 0.5
pthread_t wid[N_WRITER],rid[N_READER];
int data = 0;
int readerCnt = 0, writerCnt = 0;
pthread_mutex_t sem_read;
pthread_mutex_t sem_write;
pthread_mutex_t mutex_write;
pthread_mutex_t mutex_read;
void write()
{
int rd = rand();
printf("write %d\n",rd);
data = rd;
}
void read()
{
printf("read %d\n",data);
}
void * writer(void * in)
{
sem_wait(&sem_write);
{
writerCnt++;
if(writerCnt == 1){
pthread_mutex_lock(&mutex_read);
}
}
sem_post(&sem_write);
pthread_mutex_lock(&mutex_write);
{
printf("写线程id%d进入数据集\n",pthread_self());
write();
printf("写线程id%d退出数据集\n",pthread_self());
}
pthread_mutex_unlock(&mutex_write);
sem_wait(&sem_write);
{
writerCnt--;
if(writerCnt == 0){
pthread_mutex_unlock(&mutex_read);
}
}
sem_post(&sem_write);
sleep(W_SLEEP);
pthread_exit((void *) 0);
}
void * reader (void * in)
{
pthread_mutex_lock(&mutex_read);
{
sem_wait(&sem_read);
{
readerCnt++;
if(readerCnt == 1){
pthread_mutex_lock(&mutex_write);
}
}
sem_post(&sem_read);
}
pthread_mutex_unlock(&mutex_read);
printf("读线程id%d进入数据集\n",pthread_self());
read();
printf("读线程id%d退出数据集\n",pthread_self());
sem_wait(&sem_read);
{
readerCnt--;
if(readerCnt == 0){
pthread_mutex_unlock(&mutex_write);
}
}
sem_post(&sem_read);
sleep(R_SLEEP);
pthread_exit((void *) 0);
}
int main()
{
printf("多线程,写者优先\n");
pthread_mutex_init(&mutex_write,NULL);
pthread_mutex_init(&mutex_read,NULL);
sem_init(&sem_write,0,1);
sem_init(&sem_read,0,1);
int i = 0;
for(i = 0; i < N_READER; i++)
{
pthread_create(&rid[i],NULL,reader,NULL);
}
for(i = 0; i < N_WRITER; i++)
{
pthread_create(&wid[i],NULL,writer,NULL);
}
sleep(1);
return 0;
}
- 运行结果如下:
教材学习中的问题和解决过程
(一个模板:我看了这一段文字 (引用文字),有这个问题 (提出问题)。 我查了资料,有这些说法(引用说法),根据我的实践,我得到这些经验(描述自己的经验)。 但是我还是不太懂,我的困惑是(说明困惑)。【或者】我反对作者的观点(提出作者的观点,自己的观点,以及理由)。 )
- 问题1:对于教材中的问题关于重入的部分不是很理解。
- 问题1解决方案:上网搜索,知道了什么是重入什么是不重入,所谓可重入是指一个可以被多个任务调用的过程,任务在调用时不必担心数据是否会 出错。不可重入函数在实时系统设计中被视为不安全函数。
满足下列条件的函数多数是不可重入的:
(1)函数体内使用了静态的数据结构;
(2)函数体内调用了malloc()或者free()函数;
(3)函数体内调用了标准I/O函数。
详细可以参考链接提供的内容。
代码调试中的问题和解决过程
- 问题1:在线程的编译过程中出现下图所示问题
- 问题1解决方案:忘记了要加-lpthread编译,由于pthread 库不是 Linux 系统默认的库,连接时需要使用静态库 libpthread.a,所以在使用pthread_create()创建线程,以及调用 pthread_atfork()函数建立fork处理程序时,需要链接该库。
- 问题2:在编写读者写着问题代码时,对于信号量加1的地方,应该怎么办?
- 问题2解决方案:起初用了计数器,不方便,上网搜索发现有一个函数
sem_post
,其作用就是给信号量的值加上一个“1”,成功时返回 0;错误时,信号量的值没有更改,-1 被返回,并设置 来指明错误。 - 问题三:对于互斥锁部分应该怎样编程?
- 问题三解决方案:上网搜寻,查到了一个教程,练习代码,得到了一点启发,链接附在下面。
代码托管
上周考试错题总结
- 错题1及原因,理解情况
- 错题2及原因,理解情况
- ...
结对及互评
点评模板:
- 博客中值得学习的或问题:
- xxx
- xxx
- ...
- 代码中值得学习的或问题:
- xxx
- xxx
- ...
- 其他
本周结对学习情况
- 20155301](https://home.cnblogs.com/u/fengxingck/)
- 结对照片
- 结对学习内容
- 共同学习了第12章
其他(感悟、思考等,可选)
这周深入学习了第十二章的内容,对于其中的内容进行了练习以及实践,的确有了更深一次的了解。
学习进度条
代码行数(新增/累积) | 博客量(新增/累积) | 学习时间(新增/累积) | 重要成长 | |
---|---|---|---|---|
目标 | 5000行 | 30篇 | 400小时 | |
第一周 | 200/200 | 2/2 | 20/20 | |
第二周 | 300/500 | 2/4 | 18/38 | |
第三周 | 500/1000 | 3/7 | 22/60 | |
第四周 | 300/1300 | 2/9 | 30/90 |
尝试一下记录「计划学习时间」和「实际学习时间」,到期末看看能不能改进自己的计划能力。这个工作学习中很重要,也很有用。
耗时估计的公式
:Y=X+X/N ,Y=X-X/N,训练次数多了,X、Y就接近了。
-
计划学习时间:30小时
-
实际学习时间:40小时
-
改进情况:
(有空多看看现代软件工程 课件
软件工程师能力自我评价表)