内容简介:这是一个坑。我觉得linux的那些线程进程,之类的东西特别有意思。想把这些知识点都好好搞一下。国庆家里网超慢。CTF也做不了。[别问我跑一个盲注脚本有多慢] 然后就开始抡这个了。文章中将会包含以下内容:
这是一个坑。我觉得 linux 的那些线程进程,之类的东西特别有意思。
想把这些知识点都好好搞一下。国庆家里网超慢。CTF也做不了。[别问我跑一个盲注脚本有多慢] 然后就开始抡这个了。
文章中将会包含以下内容:
Contents
零. 前言
由于代码非常多,我在文中就不贴出来了。
代码下载地址 http://shaobaobaoer.cn/cdn/process_thread.zip
一. 进程与线程
关于概念我从简了,就记录一些自己不是很看的懂的,或者是比较重要的东西
1.1 进程
概念
进程是资源分配的基本单位,也是独立运行的基本单位。可以从进程的观念研究操作系统。
组成
- 进程控制块(PCB)
- 系统根据PCB感知进程的存在。包括如下信息
- 进程标识符PID 这个很关键
- 进程当前状态
- 进程队列指针,记录下一个PCB地址
- 家族信息,父子进程是谁.
- ...
- 程序段
- 数据块
性质
- 进程是一个拥有资源的独立单元
- 进程同时又是一个可以被处理器独立调度和分配的单元
1.2 线程
为什么需要线程
每开辟一个进程,就需要进行创建,分配IO空间,切换,撤销等一系列操作。在不同进程之间切换很麻烦。因此在进程内部再分割。让系统更好的并发执行。
概念
- 线程是进程内一个执行单元,比进程小
- 线程是进程内一个可调度实体
- 线程不能单独运行,只能包含在进程中执行。
1.3 进程与线程的C函数
进程创建函数 fork()
等待函数 wait()
创建线程 pthread_create
线程创建函数包含四个变量,分别为:
- 一个线程变量名,被创建线程的标识
- 线程的属性指针,缺省为NULL即可
- 被创建线程的程序代码 (函数)
- 程序代码的参数
pthread_create(&thrd1, NULL, (void *)&thread_function, (void *) &some_argument);
结束线程 pthread_exit
线程结束调用实例: pthread_exit(void *retval);
//retval用于存放线程结束的退出状态
线程等待 pthread_join
pthread_create调用成功以后,新线程和老线程谁先执行,谁后执行用户是不知道的,这一块取决与操作系统对线程的调度,如果我们需要等待指定线程结束,需要使用pthread_join函数,这个函数实际上类似与多进程编程中的waitpid。 举个例子,以下假设 A 线程调用 pthread_join 试图去操作B线程,该函数将A线程阻塞,直到B线程退出,当B线程退出以后,A线程会收集B线程的返回码。 该函数包含两个参数:
- pthread_t th //th是要等待结束的线程的标识
- void **thread_return //指针thread_return指向的位置存放的是终止线程的返回状态。
调用实例: pthread_join(thrd1, NULL);
1.3.1 子进程的创建与运作
// exam1.c int pid; /* fork another process */ pid = fork(); if (pid == 0) { /* child process */ printf("This is Child Process!\n"); } else { /* parent process */ printf("This is Parent Process!\n"); /* parent will wait for the child to complete*/ wait(NULL); printf("Child Complete!\n"); } root@kali:~/thread_process# ./output/exam1.o This is Parent Process! This is Child Process! Child Complete!
执行到fork()函数,分出一个子进程,完全复制父进程的所有内容,并获取一个 pid。随后,由于父进程优先于子创建,先执行父进程的代码 。与此同时,子进程会继续往下执行,执行子进程的代码。 。wait 函数是在 子进程结束后执行的。当子进程结束的时候。打印出 complete函数
1.3.2 子进程树
循环创建子进程-多分支
printf("My pid is %d my father's pid is %d \n",getpid(),getppid()); for(i=0;i<3;i++){ if(fork()==0){ printf("%d pid = %d ppid = %d\n",i,getpid(),getppid()); }else{ j = wait(&status); printf("%d : The child %d is finished.\n",getpid(),i); } } root@kali:~/thread_process# ./output/exam2.o My pid is 6783 my father's pid is 5093 0 pid = 6784 ppid = 6783 1 pid = 6785 ppid = 6784 2 pid = 6786 ppid = 6785 6785 : The child 2 is finished. 6784 : The child 1 is finished. 2 pid = 6787 ppid = 6784 6784 : The child 2 is finished. 6783 : The child 0 is finished. 1 pid = 6788 ppid = 6783 2 pid = 6789 ppid = 6788 6788 : The child 2 is finished. 6783 : The child 1 is finished. 2 pid = 6790 ppid = 6783 6783 : The child 2 is finished.
进程家族树如上所示 PS 6787 的位置是在 i=2的地方
- i = 0 6783 fork 出 6784 ;
- i = 1 6784 fork 出 6785 ; 6783 fork 出 6788
- i = 2 6785 fork 出 6786 ; 6784 fork 出 6787 ; 6788 fork 出 6789 ; 6783 fork 出 6790
一共产生了7对 8个进程,这样很没意义
循环创建子进程-单分支
root@kali:~/thread_process# ./output/exam2-2.o pid = 7052 ppid = 7051 I am parent ; my pid = 7051 and my child is 7054 pid = 7053 ppid = 7051 pid = 7054 ppid = 7051
关键思想: fork()如果发现其返回值是0 【也就是子进程返回的值】或者 -1 直接结束掉循环。这样就可以避免子进程再次创造出孙进程了。
1.3.3 进程之间的变量互不影响
root@kali:~/thread_process# ./output/exam4.o before fork. As u can see child can't change the vari and globa pid = 8106 vari == 5 globa = 4 Pid return 0 Child 8107 changed the vari and globa.vari= 4 globa = 5
通过上述代码可知: 由于fork()函数是把当前的程序再加载一次,加载后,所有的状态和当前进程是一样的(包括变量)。也就是说,fork子进程归子进程的,父进程归父进程的。两者无关系
1.3.4 线程创建实例
shaobao@shaobao-Precision-3510:~/桌面/C_BOX/socket$ gcc thread1.c -o thread1.o -pthread && ./thread1.o thread 1 create success thread 2 create success thread1:0thread1:1thread1:2thread1:3thread1:4 thread2:0thread2:1thread2:2thread2:3thread2:4 # thread1:0thread2:0thread2:1thread2:2thread2:3thread2:4thread1:1 # thread1:2thread1:3thread1:4 thread 1 return value (retval) is 10 thread 1 return value (tmp) is 0 thread 1 end thread 2 return value(retval) is 10 thread 2 return value(tmp) is 0 thread 2 end
z我运行了两次程序,结果都不一样。(#内的是第二次运行的)可见两个段线程是并发执行的。线程和老线程谁先执行,谁后执行用户是不知道的,这一块取决与操作系统对线程的调度。
二. 同步与互斥
2.1 锁
2.1.1 锁操作的概念:
一个防止他人进入的简单方法,就是门口加一把锁。先到的人锁上门,后到的人看到上锁,就在门口排队,等锁打开再进去。这就叫"互斥锁"(Mutual exclusion,缩写 Mutex),防止多个线程同时读写某一块内存区域。
一个简单防止线程(进程)间访问互斥的办法,就是在线程中加上锁。让线程锁住的时候,其他线程必须一次排队等待,当锁被开启的时候,排在首位的线程才能够进去。
2.1.2 锁操作的 C语言 代码:
- 在主线程中初始化锁为解锁状态
- pthread_mutex_t mutex;
- pthread_mutex_init(&mutex, NULL);
- 在编译时初始化锁为解锁状态
- 锁初始化 pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
- 访问对象时的加锁操作与解锁操作
- 加锁 pthread_mutex_lock(&mutex)
- 释放锁 pthread_mutex_unlock(&mutex)
2.1.2 锁操作的相关样例:
设定一个全局数据 share 。利用多线程跑函数 increase_num。如果不加锁,程序就会卡死。
// thread2.c void increase_num(void){ long i,tmp; for (i=0;i<=100000;i++){ tmp = shared1; tmp +=1; shared1 = tmp; } }
造成卡死的原因如下所示:
线程1访问到sharedi的时候,sharedi的值是1000,然后线程thrd1将sharedi的值累加到了1001,可是线程2取到sharedi的时候,sharedi的值是1000,这时候线程2对sharedi的值进行加1操作,使其变成了1001,可是这个时候,sharedi的值已经被线程1加到1001了,然而,线程2并不知道,所以又将sharedi的值赋为了1001,从而导致了卡死。
将代码改进为如下所示,此时就不会产生卡死了,因为每个线程对于 shared 变量的访问都是互斥的
````c
//thread2_lock.c
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
void increse_num(void) {
long i,tmp;
for(i=0; i<=100000; i++) {
/ 加锁 /
if (pthread_mutex_lock(&mutex) != 0) {
perror("pthread_mutex_lock");
exit(EXIT_FAILURE);
}
tmp = sharedi;
tmp = tmp + 1;
sharedi = tmp;
/ 解锁锁 /
if (pthread_mutex_unlock(&mutex) != 0) {
perror("pthread_mutex_unlock");
exit(EXIT_FAILURE);
}
}
}
### 2.2 信号 #### 2.2.1 信号的概念: 软中断信号(signal,又简称为信号)用来通知进程发生了异步事件。进程之间可以互相通过系统调用kill发送软中断信号。内核也可以因为内部事件而给进程发送信号,通知进程发生了某个事件。注意,信号只是用来通知某进程发生了什么事件,并不给该进程传递任何数据。 收到信号的进程对各种信号有不同的处理方法。处理方法可以分为三类: - 第一种是类似中断的处理程序,对于需要处理的信号,进程可以指定处理函数,由该函数来处 理。 - 第二种方法是,忽略某个信号,对该信号不做任何处理,就象未发生过一样。 - 第三种方法是,对该信号的处理保留系统的默认值,这种缺省操作,对大部分的信号的缺省操作是使得进程终止。进程通过系统调用signal来指定进程对某个信号的处理行为。 #### 2.2.2 信号操作的C语言代码: ![image_1con745ho371i1910cn35s13f1q.png-27.1kB][5] 这里的kill 实际上和系统命令中的kill 是相同的。将kill理解为杀死进程是不对的。准确的说应该是发送软终端信号 ![image_1con74t1j1bm1i5r14je4q6152t27.png-61.5kB][6] #### 2.2.3 信号操作的相关样例: 这里,我创建了一个信号,sig_usr。当用kill给它发送相关信号的时候,会有不同的内容。 ```c // exam8-1.c static void sig_usr(int); /* one handler for both signals */ int main(void) { if(signal(SIGUSR1, sig_usr) == SIG_ERR) printf("can't catch SIGUSR1"); if(signal(SIGUSR2, sig_usr) == SIG_ERR) printf("can't catch SIGUSR2"); for(;;) pause(); } static void sig_usr(int signo) /* argument is signal number */ { if(signo == SIGUSR1) printf("received SIGUSR1\n"); else if (signo == SIGUSR2) printf("received SIGUSR2\n"); }
发送SIGUSR1和SIGUSR2信号的时候,实际上就是处理方法中的第一种。从而出发了判断的操作。然后打印出了相关内容。
发送 9 信号的时候,实际上是处理方法中的第三种。信号 9 的内容是杀死该进程,于是该进程被杀死了。(如果kill 后面没有参数,则发送 信号 2 [中断信号]。
发送 NULL 信号的时候(根本没有这个信号)。系统会对这个信号不做任何处理。
shaobao@shaobao-Precision-3510:~/桌面/C_BOX/thread_process$ ./output/exam8-1.o & [2] 27581 shaobao@shaobao-Precision-3510:~/桌面/C_BOX/thread_process$ kill -s SIGUSR1 27581 received SIGUSR1 shaobao@shaobao-Precision-3510:~/桌面/C_BOX/thread_process$ kill -s SIGUSR2 27581 received SIGUSR2 shaobao@shaobao-Precision-3510:~/桌面/C_BOX/thread_process$ kill -s 9 27581 [2]- 已杀死 ./output/exam8-1.o shaobao@shaobao-Precision-3510:~/桌面/C_BOX/thread_process$ ./output/exam8-1.o & [2] 27623 shaobao@shaobao-Precision-3510:~/桌面/C_BOX/thread_process$ kill -s NULL 27623 -bash: kill: NULL: 无效的信号声明 shaobao@shaobao-Precision-3510:~/桌面/C_BOX/thread_process$ kill 27623 [2]- 已终止 ./output/exam8-1.o
2.3 信号量
2.3.1 信号量的概念:
信号量的概念是由荷兰计算机科学家迪杰斯特拉发明的,广泛的应用于不同的操作系统中。在系统中,给予每一个进程一个信号量,代表每个进程目前的状态,未得到控制权的进程会在特定地方被强迫停下来,等待可以继续进行的信号到来。如果信号量是一个任意的整数,通常被称为计数信号量(Counting semaphore),或一般信号量(general semaphore);如果信号量只有二进制的0或1,称为二进制信号量(binary semaphore)。在linux系统中,二进制信号量(binary semaphore)又称互斥锁(Mutex)。
信号量是一个确定的二元组(s,q),q是一个初态为空的队列,S表示系统中某类资源的数目。
S > 0 系统中可用的资源数目
S ≤ 0 表示被阻塞
2.3.2 信号量操作的C语言代码:
- sem_t sem; 声明
- sem_init(&sem,0,1)
- 第一个参数为信号量类型
- 第二个参数为是否在进程间共享,1为共享,0位不共享
- 第三个参数为资源的个数。(也就是上文提及的S)
- sem_init(&sem,0,1)
- 信号量有两个动作,分别是P(wait)操作,也就是 sem_wait( 信号量 -1 )
- P操作 Sem_wait(&sem)
- 信号量 -1 (s--) 如果 s > 0 则继续执行,如果 s ≤ 0 则阻塞
- V操作 Sem_post(&sem)
- 信号量 +1 (s++)如果 s > 0 则继续执行,如果 s ≤ 0 则阻塞
- sem_destroy(&sem)
- 信号量的销毁
2.3.3 信号量操作的样例:
互斥
为了实现互斥问题,一般来说,信号量代码格式如下:实际上就是简化版本的生产者与消费者问题。
也就是要求是P1的代码段和P2的代码段不同时运行。初始的资源量1。也就是能够保证P1和P2中只能有一个能够先运行起来。
若 P2 先执行则会将S变为0,从而堵死P1()的代码段【此时当P1运行到P(&sem)的时候就会锁住了】。
当P2的代码段执行结束的时候,会将S重新置为1,此时,P1中的P(&sem)终于等待到了S≥1.P1的代码段就会继续往下走了。可能在这里可以通过锁来解决,不过用信号量来解决很明显更加直观易懂。
同步
为了实现同步问题,一般来说,信号量代码格式如下:
这里的要求是 S1 -> S2。初始的资源量为0。也就是没有 S1 资源。
当 P1 P2 同时执行的时候,若 P2 先执行,则会因为S≤0而阻塞,当S ≥ 1 的时候,P2才能继续执行。P2不断阻塞,直到P1完成一个S1,将S++。才能让P2继续执行。
PS : 关于信号量机制,下面的经典问题中会详细提及。
2.4 管道
管道通信方式的中间介质是文件,通常称这种文件为管道文件。两个进程利用管道文件进行通信时,一个进程为写进程,另一个进程为读进程。写进程通过写端(发送端)往管道文件中写入信息;读进程通过读端(接收端)从管道文件中读取信息。两个进程协调不断地进行写、读,便会构成双方通过管道传递信息的流水线。
管道分为匿名管道和命名管道。
(1)匿名管道:管道是半双工的,数据只能单向通信;需要双方通信时,需要建立起两个管道;只能用于父子进程或者兄弟进程之间(具有亲缘关系的进程)。
(2)命名管道:可在同一台计算机的不同进程之间或在跨越一个网络的不同计算机的不同进程之间,支持可靠的、单向或双向的数据通信。
2.4.2 管道操作的C语言代码:
2.4.3 管道操作的样例:
这里,我建立了一个管道
//exam5-1.c int pipe_default[2]; int main() { pid_t pid; char buffer[32]; memset(buffer, 0, 32); if(pipe(pipe_default) < 0) // 建立管道 { printf("Failed to create pipe!\n"); return 0; } if(0 == (pid = fork())) { //child sleep 5 to wait buffer close(pipe_default[1]); // 关闭管道的写端 sleep(5); if(read(pipe_default[0], buffer, 32) > 0) // 从管道中读取内容 { printf("Receive data from server, %s!\n", buffer); } close(pipe_default[0]); } else { // parent send buffer close(pipe_default[0]); // 关闭管道的读端 if(-1 != write(pipe_default[1], "hello", strlen("hello"))) // 将内容写入管道 { printf("Send data to client, hello!\n"); } close(pipe_default[1]); waitpid(pid, NULL, 0); // 等待子进程结束 } return 1; }
2.5 消息队列
2.5.2 消息队列操作的C语言代码:
2.5.3 消息队列的样例:
2.5 死锁与饿死
2.6 僵尸进程与孤儿进程
参考文章 https://www.cnblogs.com/Anker/p/3271773.html
写的很好,可以参考里面的例子代码。
2.6.1 相关概念
在unix/linux中,正常情况下,子进程是通过父进程创建的,子进程在创建新的进程。子进程的结束和父进程的运行是一个异步过程,即父进程永远无法预测子进程 到底什么时候结束。当一个进程完成它的工作终止之后,它的父进程需要调用wait()或者waitpid()系统调用取得子进程的终止状态。如果操作不当,就会有僵尸进程与孤儿进程
孤儿进程:一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。
僵尸进程:一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵死进程。
2.6.2 如何解决孤儿进程
孤儿进程这个重任就落到了init进程(一般是进程号为1 的进程)身上,每当出现一个孤儿进程的时候,内核就把孤儿进程的父进程设置为init,而init进程会循环地wait()它的已经退出的子进程。这样,当一个孤儿进程凄凉地结束了其生命周期的时候,init进程就会代表党和政府出面处理它的一切善后工作。测试关键代码如下:
// exam6-orphan.c pid_t pid; //创建一个进程 pid = fork(); if (pid == 0) { printf("I am the child process.\n"); //输出进程ID和父进程ID printf("pid: %d\tppid:%d\n",getpid(),getppid()); printf("I will sleep five seconds.\n"); //睡眠5s,保证父进程先退出 sleep(5); printf("pid: %d\tppid:%d\n",getpid(),getppid()); printf("child process is exited.\n"); } //父进程 else { printf("I am father process.\n"); //父进程睡眠1s,保证子进程输出进程id sleep(1); printf("father process is exited.\n"); }
运行结果如下:
root@kali:~/thread_process# ./output/exam6-orphan.o I am father process. I am the child process. pid: 8743 ppid:8742 I will sleep five seconds. father process is exited. root@kali:~/thread_process# pid: 8743 ppid:1 # <--- 孤儿进程。已经被 init 进程“收养”了 child process is exited.
由于系统会自动处理孤儿进程,所以孤儿进程是无害的。
2.6.3 如何解决僵尸进程
我们先写一个僵尸会产生僵尸进程的程序,如下所示,
// exam6-zombie-test.c pid_t pid; pid = fork(); if (pid == 0) { printf("I am child process.I am exiting.\n"); exit(0); } printf("I am father process.I will sleep two seconds\n"); //等待子进程先退出 ssleep(2); //输出进程信息 system("ps -o pid,ppid,state,tty,command"); printf("father process is exiting.\n"); return 0;
由于子进程先行退出。父进程却没有处理子进程留下的那些数据结构(比如说我们之前说的PCB,代码段等)导致子进程还是残留在父进程中。可见僵尸进程的罪魁祸首就是这个懒惰的老爹。为了能够正确处理僵尸进程。有两种方法,一种是给子进程发送信号,另一种是在父进程外面再包一个进程,这个进程充当者 Init 的作用,把僵尸进程变为孤儿进程。来解决这样的问题。
三. 操作系统经典问题
3.1. 生产者消费者问题
3.2. 读者写者问题
在读者写者问题中,有一个许多进程共享数据区,有一些只读取数据区的进程(读者)和一些只向数据区写数据的进程(写者)
为了实现上述功能,我们需要满足以下条件:
- 任意读者可以同时读取文件
- 一次只能有一个写者写文件(写者必须互斥)
- 而以下三个条件中,只能满足一个。也就延伸出了三种算法
- 如果一个读者在读操作,则禁止任何进程写(读者优先)
- 如果将在运行的人看做队头,在后面依次阻塞等待的人看做顺序队列,当队头运行结束的时候,队头后面的线程(进程)开始运行。(顺序优先)
- 如果一个写者在写操作,则禁止任何进程读写。(写者优先)
3.2.1 读者优先
在写读者优先算法的时候,我们需要两个信号量集mutex和rmutex和一个整形变量readcount。Mutex 变量用于控制进程之间的互斥访问。Rmutex 用于控制 readcount 之间的互斥访问。Readcount 用于控制读者的个数。当读者个数为0的时候,才能允许写者操作。
伪代码如下:
运行结果
... Writer writing data stack[4] = 88 Writer writing data stack[5] = 87 Reader Count 1 ; Reading data ...Stack[1] = 72 Reader Count 1 ; Reading data ...Stack[4] = 88 Reader Count 1 ; Reading data ...Stack[3] = 95 Reader Count 2 ; Reading data ...Stack[4] = 88 Reader Count 2 ; Reading data ...Stack[1] = 72 Reader Count 1 ; Reading data ...Stack[4] = 88 Reader Count 2 ; Reading data ...Stack[4] = 88 Reader Count 2 ; Reading data ...Stack[6] = 0 Reader Count 1 ; Reading data ...Stack[6] = 0 Writer writing data stack[6] = 59 Writer writing data stack[7] = 22 Reader Count 2 ; Reading data ...Stack[5] = 87 Reader Count 3 ; Reading data ...Stack[1] = 72 Reader Count 2 ; Reading data ...Stack[4] = 88 Reader Count 1 ; Reading data ...Stack[8] = 0 Reader Count 1 ; Reading data ...Stack[3] = 95 Reader Count 1 ; Reading data ...Stack[1] = 72 Reader Count 1 ; Reading data ...Stack[1] = 72 Reader Count 1 ; Reading data ...Stack[6] = 59 Reader Count 1 ; Reading data ...Stack[3] = 95 ...
三个读者,两个写者,可能会一次输出6条读取信息,因为读者优先于写者
3.2.2 公平竞争
为了解决顺序排队的问题。在读者优先的基础上,需要增设信号量wmutex 用于表示是否存在正在写或者正在等待的写者,如果存在,则禁止新的读者进入。
伪代码如下:
运行结果
... Writer writing data stack[2] = 69 Writer writing data stack[3] = 88 Reader Count 1 ; Reading data ...Stack[3] = 88 Reader Count 1 ; Reading data ...Stack[2] = 69 Reader Count 1 ; Reading data ...Stack[1] = 69 Reader Count 1 ; Reading data ...Stack[2] = 69 Reader Count 1 ; Reading data ...Stack[3] = 88 Reader Count 1 ; Reading data ...Stack[4] = 0 Writer writing data stack[4] = 40 Writer writing data stack[5] = 31 Reader Count 1 ; Reading data ...Stack[4] = 40 Reader Count 1 ; Reading data ...Stack[2] = 69 Reader Count 1 ; Reading data ...Stack[4] = 40 Reader Count 1 ; Reading data ...Stack[2] = 69 Reader Count 1 ; Reading data ...Stack[6] = 0 Reader Count 1 ; Reading data ...Stack[3] = 88 Reader Count 1 ; Reading data ...Stack[5] = 31 Reader Count 1 ; Reading data ...Stack[5] = 31 Reader Count 1 ; Reading data ...Stack[6] = 0 ...
三个读者,两个写者,可以发现读者和写者穿插运行,这是因为顺序排队的原因。
3.2.3 写者优先
为了解决写者优先问题。需要额外增加信号量 readable。控制写者到达的时候锁住所有读者。同时,要用之前读者优先的计数问题,来解决写者数量的统计问题。
伪代码如下:
运行结果
... [*] Now writercount = 2 [+] Writer writing data stack[8] = 63 [*] Now writercount = 1 [+] Writer writing data stack[9] = 19 Reader Count 1 ; Reading data ...Stack[4] = 91 Reader Count 1 ; Reading data ...Stack[9] = 19 Reader Count 1 ; Reading data ...Stack[2] = 39 [*] Now writercount = 1 [+] Writer writing data stack[10] = 100 [*] Now writercount = 1 [+] Writer writing data stack[11] = 84 [*] Now writercount = 1 [+] Writer writing data stack[12] = 9 [*] Now writercount = 1 [+] Writer writing data stack[13] = 90 Reader Count 1 ; Reading data ...Stack[2] = 39 Reader Count 1 ; Reading data ...Stack[13] = 90 Reader Count 1 ; Reading data ...Stack[4] = 91 [*] Now writercount = 2 [+] Writer writing data stack[14] = 51 [*] Now writercount = 1 [+] Writer writing data stack[15] = 101 Reader Count 1 ; Reading data ...Stack[10] = 100 Reader Count 1 ; Reading data ...Stack[6] = 20 Reader Count 1 ; Reading data ...Stack[6] = 20 [*] Now writercount = 1 ...
三个读者,两个写者,有时候两个写者会打印4条写入信息,因为写者优先于读者
3.3. 哲学家进餐问题
3.4. 理发师问题
3.5 银行家算法
5. 扩展问题
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- APUE 学习笔记——进程关系
- APUE 学习笔记——进程控制
- APUE 学习笔记——进程间通信
- 操作系统学习笔记-7:进程通信
- OS-操作系统学习笔记-5:进程同步与进程互斥(二):信号量机制
- Nginx源码阅读笔记-Master Woker进程模型
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
科学的极致:漫谈人工智能
集智俱乐部 / 人民邮电出版社 / 2015-7 / 49.00元
集智俱乐部是一个从事学术研究、享受科学乐趣的探索者组成的团体,倡导以平等开放的态度、科学实证的精神进行跨学科的研究与交流,力图搭建一个中国的“没有围墙的研究所”。这些令人崇敬的、充满激情与梦想的集智俱乐部成员将带你了解图灵机模型、冯•诺依曼计算机体系结构、怪圈与哥德尔定理、通用人工智能、深度学习、人类计算与自然语言处理,与你一起展开一场令人热血沸腾的科学之旅。一起来看看 《科学的极致:漫谈人工智能》 这本书的介绍吧!