25. linux系统基础

信号状态 必达 未决 产生

信号的处理方式有五种,默认动作 忽略 捕获 其中有两个信号,不能够被忽略 阻塞和捕获是哪个?

sigkill 和sigstop 你把这两个信号记住相当于常识,

信号的特质,

信号四要素,每个信号都有编号,  每个信号都有名称,还有产生的事件,也就是说这个信号是怎么产生的吧?

实际上也就是说每一个信号只有在特定场合下才会产生啊?你不要乱发送信号,还有默认处理动作有这几种啊?

 其实很多信号默认处理动作都是终止,
信号相关的函数 这几个都很简单

 signal函数是完成信号注册,注册的时候,这个信号处理函数,相当于给内核注册的,把某一个信号的处理函数告诉内核,那么你告诉内核的是函数的首地址吧?
kill  abort  
alarm 时钟 setitiemer 他也是时钟
但是setitiemer他可以周期性触发,但是alarm他只能触发一次啊?

关于这setitiemer 有同学说还有alarm遇到这样一个问题,这个alarm是不是会产生信号啊?setitiemer是不是也会产生信号啊?凡是阻塞的函数,只要遇到信号马上中断,举个例子 sleep20

这个时候到10秒的时候,是不是还剩10秒的时候,这个sleep已经结束了?到10秒的时候产生的信号,这个sleep马上中断,剩余的10秒就不再sleep了,后面讲网络编程 还会讲这样的一个东西,

如果说read函数 是不是阻塞的?讲文件io 是第3天,

这个函数 我们原来调用这个read啊? read读这个标准输入输出是不是阻塞的?咱们在外面发出一个信号,是不是立刻返回就知道了,这个是一开始阻塞吧?只要不输入是不是一直阻塞?

是不是起来了?是不是阻塞的?

发一个信号 kill-INT 1853

2号信号 是不是ctrl+c啊?

看一下是不是返回了? 这个函数15 另返回了,这个16 还会执行吗?当这个函数 默认处理的动作是什么啊?

默认处理动作是不是终止啊?cdint 默认处理信号是终止,所以你这 没有,他已经终止了,

这个是不是 read函数相当于也马上中断了?

找一个合适的例子,第七天,我们这个alarm是不是有啊?这个sleep10  

sleep期间,两秒钟之后是不是会产生一个信号啊?这个sleep10  还会阻塞10秒吗?他就不再阻塞10秒了,立刻这个进程就结束了

 是不是因为这个这个马上就被中断了?9-12  执行完这个之后 紧接着就执行return0了,

因为sleep10是不是被这个信号中断了?

他不会等10秒就退出了, 这个后面讲网络编程会说到,这个阻塞函数遇到信号都会被中断,那么产生一个错误叫什么错误呢?EINTR中断函数调用 给信号有关系,第7节

man 1  2  3   5 7 man5 pasworld 是不是可以看pasworld文档的格式 

7可以查看这一个章节的相关信息,只要是被信号中断的,那个函数的返回值,就会返回一个-1,或者是一个错误的值,errorno会设置为EINTR。

settitimer应该说他比alarm优势在哪呢?他是不是可以周期性发送一个信号啊?这个用的时候麻烦就麻烦到他的结构体 套结构体,赋值的时候可能麻烦一些,但是用习惯了 是不是也一样 啊?

首先信号集你得知道这两个信号集到底是什么关系?这个我给大家讲了两三遍吧?最后你自己有没有总结总结?

这两个信号集,未决信号集的话,刚刚启动一个进程里面 如果没有任何信号产生的话,这个未决信号集里面有东西吗?大家注意,这个未决信号集是后来触发的,只有有信号之后,这个信号才有可能是未决信号,如果没有信号的话,没有信号产生,未决信号集里面是不会有任何东西的,未决信号集 和阻塞信号集有什么关系呢?

未决信号集产生 主要是由于信号阻塞照成的,这个信号解除阻塞之后,这个未决信号是不是还是需要被处理啊?

 这里面 未决信号集也好,还是阻塞信号集也好,他们使用的数据类型是不是都是sigset_t类型的啊?这个类型实质上 我给大家也分析了,他是不是一个ansetlong类型啊?longint 他是不是无符号长整型吧?是8个字节,8*8=64 这里面能够表示的信号是不是只有64个啊?

因为每一个位是不是只表示一个啊?

信号集相关的函数,这一个一个不再说了,直接打开例子 在这个代码里面我们是不是获取的是未决信号集,并把这一个信号,某一个信号是否在这个未决信号集里面打印出来,

是不是要设置阻塞信号 才能在未决信号集里面 留下信号啊?

//定义信号集变量sigset _t set;sigset_t oldset; 首先我们定义了两个变量,进行初始化,信号集相关函数用的时候呢,首先定义变量 然后进行初始化,这边是不是类似于mmsite啊?接下来 将这两个信号加入到信号集当中去,执行到这以后呢30  31阻塞信号集里面并没有数据,那么sigprocmask(到底有没有要看这个函数的调用了 35 这个函数调用完以后,那就意味着这个set集合里面30 凡是为1的这个信号,是不是都加入到了阻塞信号集里面去了,那么也就是说这个 函数的调用才是真正的和内核打交道的35,

这些30 31是不是都不是啊?这些是不是都是操作本地变量啊?

在while语句循环里面,不停的获取未决信号集, 调用sigpending(&pend); 45

这个是不是也和内核打交道啊?内核将未决信号集 他的数据给你copy到本地变量里面来了,大家注意这个pend 是本地变量 是不是在栈上啊?用这个变量的时候,在循环里面用这个变量的时候每次都要初始化,数组在这个while语句循环里面是不是也是这样的?也是while set

然后咱们在这个循环里面 打印1-31号信号,判断这个信号是不是在这个集合当中,应该调用一个函数叫sigismember到底在不在这个里面其实 本质上还是位置操作啊? 其实在 也就是那个对应的位置上他的值是不是为1 啊  不在为0,

 接下来咱们做这样一个操作,连续循环10次 我们就解除阻塞啊?在这解除阻塞有两种方法一种是

//sigprocmask(SIG UNBLOCK用这种方式 是不是最直观啊?63 

一种是用这一种方式 是不是sigprocmask(SIG SETMASK 64 赋值的方式啊?那么赋值的方式你需要把原有的这个设置 再设置回来就可以了?

所以 这个sigprocmask第三个参数,是不是就是获取原有设置的参数啊?这是一个输出参数

完成之后,最后继续保持阻塞

sigaction函数的用法相对来说比signal复杂一些,但是功能要强大一些,

 这个函数用的时候一般用三个 第一个信号处理函数 我们传值的时候是不是只传一个函数的名字就可以了?

第二个 我们用sigset_t sa_mask;/ 这个函数的目的是在信号处理函数执行期间,我们要阻塞某些信号是不是可以加载这个集合当中啊?

执行这个 你想阻塞某些信号 其实大家注意听, 这一块呢 并不是说把某些信号加入到了阻塞信号集,这个是不是没有和内核打交道啊?你要想设置到内核里面去 必须调用哪个函数啊?sigprocmask 这个调了吗?没调吧? 所以他只是临时性的阻塞一下,那么这个信号处理函数执行完以后是不是他会继续 处理indes信号啊?

还有sa flags;   还有这三个参数 这个已经废弃了,*sa restorer 

一个是我们信号处理函数, 一个是我们需要阻塞哪些信号, 那么提到这我给你说一下 第一个信号处理函数 在执行期间,这个信号本身是被阻塞的,

如果说信号函数在执行期间,这个信号本身又产生了多次,最后的话是不是只执行一次啊?这里面说明一点 信号不支持排队,其实你通过我们画的图是不是已经看出来了?那个里面都是位图 并没有记录信号产生了多少次,所以他能知道执行几次吗?并不知道 他只知道执行一次, 反正有嘛,有几次不知道,这个是不是赋这三个值啊?

接下来调用sigaction(SIGINT, 第一个参数是不是你想注册哪个信号处理函数啊?信号的编号,这个cint 是不是2号啊? 后面是null 表示的是不是先前的设置啊?原来一个老的设置,在这你关心吗?

所以设置为null就可以了,这个sigaction我给大家再看一个手册,man sigaction

这个手册 慢慢要熟悉起来,sa_handler这个是函数名 我们一般设置 这个除了函数名以外 还可以设置两个宏 一个叫SIG_DFL 这个信号默认处理是不是default 啊 每一个信号是不是都有一个默认处理动作啊?如果你这样设置其实还不如不写呢,是不是可以不写啊?

SIG_IGN 忽略的信号,那么忽略和阻塞 到底是什么区别?忽略你就当成他没有发生一样,那阻塞是不是他只是说暂时不处理啊?如果解除阻塞 这个信号还是需要被处理的,这两个宏,你比如说在别人的代码里面看到你不用 管他就可以了,

举个例子 假如说 父进程 循环创建3个子进程, 你这个父进程 你注册的时候你第三个参数你就写了个这玩意SIG_IGN , 是不是相当于你这个父进程忽略这个sigtip信号啊?忽略他还会产生僵尸进程吗?

这个参数改成 SIG_IGN 忽略  看看会不会产生僵尸进程,这个sigaction(用法就这么些,如果你是在linux上开发的话呢,比如说你这个版本比较固定的话呢,你用signo一般也没事,但是人家手册上给你说了,他在不同的版本表现的形式不一样,建议你还是用sigaction代替,

 SIGCHLD 信号

是什么条件下产生的,

  
子进程结束的 时候

子进程收到 SIGSTOP 信号

当子进程停止时,收到 SIGCONT 信号

如果说父进程收到信号呢,收到进程暂停 才收到这个信号,那么你说调用这个位置他能成功吗?

不能回收,回收的前提条件是子进程确实死了,没有死能回收吗?

父进程可以通过这个信号,就有了回收子进程的时机,那么你说这个父进程 实际上它并不知道你这个子进程什么时候退出的,

它可以收到这个信号,收到这个信号之后呢,它就知道有子进程退出了,这样的话它是不是就可以调用waitpid进行回收啊?

这样的话你这个父进程就不用阻塞等待了,或者是一直在死循环里面了

为什么我要在fork之前阻塞 SIGCHLD信号,防止调用sigaction这个函数 完成注册以前,三个子进程完全退出啊?如果三个子进程已经全部死掉了,你才完成注册 这个时候你的这三个子进程不会收到SIGCHLD信号的,这个是咱们试过了,

这个为什么加break?

 不再让子进程 继续fork子进程,确保父进程创建出来的孩子进程 都是兄弟关系,这个我们父进程做的事情就是完成对子进程的回收,所以说为了避免 在三个子进程 全部退出了,我们才完成注册的,所以第一个我们先将SIGCHLD信号阻塞,

然后完成90注册之后,再解除阻塞

 假如说我三个子进程全部完成了,我才完成注册,那么你说这个 我还能够对这三个子进程进行回收吗?因为这个至少来说 我们这个未决信号集里面保留一个位置啊? 未决信号集里面有这么一个标识是1,表明这个sigtask信号需要被处理吧?

但是呢由于信号不支持排队,如果说你这个while循环你不加的话,会怎么样?会产生两个僵尸进程,这个咱们说过了,

 所以 我们可以把这个回收的操作,waitpid写在while循环里面其目的是收到一个信号 ,然后循环回收多个,不论是你有先有后的一个一个退出,还是说一个退出了,另外两个分别退出了,还是说三个全部退出了都能解决,无非是你这个循环多循环1次啊?什么情况下,我这个就回收完了呢?

是不是要看他的返回值啊?如果wpid==-1 是不是没有子进程了?或者是如果说子进程还活着呢,假如说我有一个子进程退出了,是不是这个函数也会执行啊?比如说有第一个子进程退出了, 这个函数也会执行,那么接下来,它再循环一次是不是他又看另外两个还活着呢是不是到这25就break了?

还有三个子进程 假如说全部一起退出呢?也没有关系,那while循环里面呢,它连续给你回收3个,第四次再循环的时候呢,这个是不是也退出了?27-30

这个代码要求你敲一遍

忽略就是不处理,就是当他没发生,他跟阻塞就不一样了吧?

换个函数试一下

 只要你不调用未决pid回收,只要父进程没有回收,他就有僵尸进程产生

 你只有调用未决pid是不是才能回收啊?

92 第二个参数就相当于回调函数,

 SIG_DFL默认也是不处理,是不是未产生..啊?你写个默认是不是相当于没写啊?

举个例子 是不是ctrl+c 会参生cdint信号啊?你可以把这个忽略,这个时候你再按ctrl+c 这个界面就不好使了,是不是就相当于这个信号没有发生一样啊?这个是可以的,但是那种的话你僵尸进程的话,你如果没有调用waitpid回收,他肯定会产生僵尸进程,当然你这个父进程退出是不是也可以啊?父进程退出你也可以不用调用waitpid 那么他是不是就被1号进程回收了?

 

 通常独立于控制终端 这个1号进程 他依赖于终端吗?凡是依赖终端的都可以用ctrl+c给他杀死吧?

它不依赖于终端,而且init进程, 也没有往终端输出日志啊?是不是没有按ctrl +f啊?

并且周期性地执行某种任务或等待处理某些发生的事件 只要是守护进程 一般都是while1 里面

是不是等待着某个事件的发生啊?你现在可能对这个还理解的不太深刻呢,等咱们讲网络编程的时候,咱们讲的都是服务,这个服务可以把他变成后台进程,等待处理某些发生的事件,那么再比如说ftp  cocalhost 是不是接下来让你输入用户名呢?你 一执行ftp呢,你这个linux操作系统上 他有一个ftp服务,我这样写的话是不是相当于我是客户端啊?那个服务他等待一个事件,什么事件呢?有客户端连接他  这就是一个事件,他是被动的,你客户端主动连接他啊?

就像淘宝的服务一样,你是不是买东西,他那才给你提供服务啊,你不买东西有吗?是不是,客户端主动行为,服务器是被动行为,

 刚刚登录上来的时候,登录到用户 下的家目录下来的,我家目录是不是,/home/itcast 怎么退出

 ftp可以上传文件和下载文件,

一般采用以d 结尾的名字,如 vsftpd   vs非常安全的,一般守护进程以d结尾,但是不是绝对的,我举个例子,自己写了一个守护进程你是不是名字,可以随便取啊?

我给你讲这个服务,怎么看这个服务呢?怎么看这个服务有没有活着啊?

服务说的简单一些就是进程,不要把他想的太复杂了,服务就是后台进程, 有没有活着 啊?

是不是活着呢?你这个服务在哪个目录下啊? sbin下保持的是什么啊?是不是系统的常见命令或者工具啊?找个后面是他的参数吧?/etc/vsftpd.conf 其实在这是一个配置文件,

这个是不是我们的grep那个进程啊,这个才是服务名叫vsftpd

Linux 后台的一些系统服务进程,没有控制终端,不能直接和用户交互

不受用户登录 注销的影响, 这个咱们写完一个守护进程例子的时候呢,你自己可以试一下,你把这个守护进程起来以后呢,你把这个用户退出,退出之后你再登录进去,你再看这个守护进程是不是还活着呢,是不是就测出来了 如果说你这个进程不是守护进程,是普通进程,

举个例子 sleep 100是不是普通的一个进程啊?肯定不是守护进程吧?这个时候如果说你把这个关掉了, 这个是不是死掉了? 但是守护进程不一样,你守护进程 起来以后,你把这个关掉它还活着呢,为什么啊?因为它不依赖于终端,

不受用户登录 注销的影响,一直在运行着,他们都是守护进程。shell 他是不是守护进程啊 这个shell 是不是有控制端啊?不严格的时候 他是不是也在一直运行啊?是不是也不退出啊?除非你让他退出 当然他是占用一个控制端的

ftp服务器;nfs 服务器等。这个了解一下就行了,把这个大致的看一看,

咱们看看有哪些特点?第一个呢 其实这个特点都是这里面总结出来的,

第一个 Linux 后台服务进程  

独立于控制终端  不受控制段影响,也就是说你既不能通过终端给他输入参数,输入数据,你也不能从控制端得到他的输出数据,

周期性的执行某种任务  你这个时候你这个进程是肯定不能退出的,如果退出了,还叫守护进程吗?不叫守护进程了,

 不受用户登陆和注销的影响  这个你可以试


一般采用以 d 结尾的名字  他说一般以d结尾,但是不是绝对的吧? 这个名字我们可以自己定,

进程组是一个或者多个进程的集合 我们系统下的每个进程都属于一个进程组,如果说这一个组里面只有一个进程 都它是不是自己就是组长了?

  

 引入进程组是为了简化对进程的管理。

举个例子,那么你系统在关机的时候,那么系统在关机的时候 这个系统,有些进程会给其他的进程发送信号,让他退出,你不信你可以看一下,肯定有seningsecno那些东西 你要注意看你能看到这些东西, 那你是给一个进程组发一个信号好,还是给所有的,就是一个组里面所有的 进程发送信号好?其实咱们昨天 给你讲例子的时候呢,是不是给你讲过了?

kill 那个函数 我杀死进程的时候,我如果说那个指定为0,指定-1是不是不一样啊?指定为0是不是可以杀死组当中所有进程啊?指定-1是不是可以发送给 它有权限发送所有进程啊?这样的话 其实它可以大大减少这个对进程的管理,你发信号的话 你给10个进程发,和给一个组发一个信号,是不是不一样啊?哪个效率高?况且的话你这个进程数可不止10个8个的 

 是不是199个?当然除了有些是我们后来开的,这些是不是我们自己开的,这些是不是也是一个一个进程啊?

bash  shell 也是进程,是不是 有很多个啊?一大堆,那么你说这个组管理 按理来说按照组管理来说是不是比较方便一些啊?这个是怎么理解啊?你可以想象一下 可以想象一下我们这个社区 社区是不是有的时候,以户为单位,以整栋楼为单位,是不是干什么事的时候方便管理啊?类似的,

进程组 ID==第一个进程 ID (组长进程)这个咱们可以 我给大家举例子的时候是不是给你举了一个例子啊?一个父进程 创建三个子进程,这个父进程的id是不是就是组长id啊?这个很明显 是这样的,

再比如如果说这个组里面,它的组长进程退出了,那么你说这个进程组还有组长吗?没有组长 但这个组还存在,只有说这个组里面,最后一个进程退出了,那么这些组 才真正的消失了,

 kill -SIGKILL-进程组ID(负的) 如何杀死一个组呢?是不是 这个可以用负的啊?

是不是发送给一个组啊? 如果等于-1的话,是不是等于所有进程啊?这个是不是用-1是不是杀死这个man啊? 用这个0杀死一个组里面,这个你自己试一下就可以了,如果你这样写的话,不但可以杀死一个组里面的,是不是也可以杀死其他进程啊?

我记得昨天是不是试过了?把所有终端杀死了?那么只要进程组中有一个进程存在,进程组就存在,与组长进程是否终止无关。

进程组生存期: 从进程组创建到最后一个进程离开

也就是进程组里面有很多个进程,那么这个组生成周期是这样的,从进程的第一个,这个组里面第一个进程产生到,到最后一个进程离开,那么也就是这个组的生存期

这些东西 你就了解一下就可以了,一般我写代码的时候用不着它

 一个会话是一个或多个进程组的集。 一个会话里面至少有一个进程组,待会咱们写守护进程的时候,出现这种情况了, 一个会话里面只有一个进程,这一个进程既是会长也是组长,每一个会话都有一个会长,怎么看这个进程的会话组id啊?

 还记得这四组 每组代表什么意思吗?这个是不是父进程啊  这个是他自己 ,这个是组 这个是会话吧?如果记不住,看上面ppid  父进程pid  pgid是它自己,  sid 是组  tty是会话,

command 是终端, 后面的是命令

 init命令是不是1号进程啊? 它的组id  是不是也是1 啊. 会话id也是1,是不是完全一样啊?也就是说这个会话里面是不是只有一个进程啊?这个组里面是不是也只有一个进程啊?它的父进程是不是0号进程啊?init的父进程是不是0号进程?0号进程的 父进程还有吗? 相当于最原始的进程

创建会话的进程不能是进程组组长  这是一个硬性条件,硬性规定 就是说你想创建会话,你必须保证你这个进程不能够是组长,我举个例子,我父进程 我循环创建3个子进程,这个组长是父进程, 父进程不能创建会话, 如果想创建会话怎么办?起码你不能让这个父进程作为一个会话的会长,怎么办?你可以让这个父进程退出,父进程退出之后,然后让子进程去创建会话,是可以的,

创建会话的进程成为一个进程组的组长进程,同时也成为会话的会长

 也就是说这个进程创建了一个会话了,进程也就成为了组长进程 同时也是会长 是不是身兼数职啊?

需要有 root 权限(ubuntu 不需要) 有的系统是不需要的,

新创建的会话丢弃原有的控制终端 这个是不是和我们的守护特点一致啊?

只要是守护进程,它就不依赖于任何终端

建立新会话时,先调廉fork,父进程终止,子进程调用 setsid函数

调用这个setsid函数就相当于创建一个会话的意思

我父进程 我fork出一个子进程来,那么这个子进程是组长进程吗?

不是,即便 是你这个父进程退出,它是不是这个子进程的组长是不是也不是它自己啊?它是不是也是原来的父进程啊?它不是组长了,它能不能创建会话?是不是满足这个条件了?因为创建会话的要求 是你只要不是组长就行 

很显然 父进程 他不能创建会话,如果说你仅有一个进程的话,比如说我就普通的一个进程,

比如说我在这里执行了一个sleep 100 你说这个sleep 100 的组长是谁啊?

 

组长是他自己, 但是大家想一下 为什么 一个普通进程如果说你不创建子进程,他自己根本就不能创建会话的,知道为什么了吗?因为他自己是组长,你这个sleep 100 是不是组长啊?

组长能创建会话吗?不行,违背了我们这个条件了,所以说一般是这样的,他创建一个子进程,然后他退出,那个子进程他肯定不是一个组长吧?所以说他可以调用setsd来创建用户吧?

可以使用 ps ajx 来查看进程组 ID 和会话 ID 可以fork 出几个子进程,然后查看进程组 ID 和会话ID

通过ps ajx来查看多个子进程和父进程 他是不是在一个组里面,是不是在一个会话里面

 图表明,一个组里面 是不是有多个进程啊?多个组是不是组成一个会话啊? 我可以把一个家庭是不是可以看成一个组啊? 每一个人是不是相当于一个进程啊?这个整栋楼 或者一个小区呢?是不是相当于有多个组啊? 因为有多户嘛,那这样的话,你认为他是一个会话,可以吧?

进程组和会话的概念  咱就说到这,

比如说我这个父进程 就创建出来一个子进程,那么我这一个子进程既是会长也是组长啊?也就是说我这个会话里面,或者我这个组里面是不是只有一个进程啊?那么只要是创建了会话,只要是创建了会话,他就是脱离了控制终端的影响,

写的时候给你测一下你就知道了,你就看不到他了,你只能用ps asaux 看一下他的pid了,看不到他了,因为他已经不在终端表面显示了吧?在后台执行了,关于进程组 和会话的概念呢,咱们就说这么多,基本概念你只需要理解就可以了,不要死记硬背,背的和理解的完全不一样

 

 接下来咱们讲一下如何创建守护进程,咱们前面讲的是不是都是守护进程一些特点,和基本概念 是不是?接下来咱们看一下 如何创建守护进程啊?

 这有一个模型,大家参照这个模型写就可以了,一共分了6步,

1.fork 子进程,父进程退出 这样做的目的,那么这个子进程继承了父进程的进程组 ID,首先问一下大家 在这个进程组里面我这个fork 一个父进程 fork一个子进程,那么这一个组里面 这个组长是谁?是不是父进程?首先 保证了一点,我们这个子进程肯定不是组长就好了,这个没问题吧?也就是说  我这个父进程退出了,我们这个组里面是不是只有一个进程了?当然目前来说 这个组的组长还是原来的父进程啊?这个没有关系,第一步呢就是这个,这么写就可以了,既然说你这个子进程他不是组长进程,那么他是不是就可以调用setsid函数了?那么调用这个函数的目的是什么?创建一个会话,创建会话的进程不能是进程组组长 

创建一个会话以后呢,是不是会形成会话啊?同时也会形成一个新的组,这个待会咱们  测试一下 是不是这样的,第二个 子进程调用setsid 函数创建新会话  那么创建一个新的会话以后呢,达到了后面几个效果,第一个效果就是该进程成为新会话的首进程 因为他的父进程是不是已经死了?这个进程是不是相当于在这个组里面是第一个进程啊?是会话的会长

第二个称为一个新进程组的组长进程,是进程组组长

那么你说这个会话的会长呢,会话id是不是他自己啊?组长id是不是也是他自己啊?.

后来 第三条是 他不受控制终端的影响 只要成了会话,他就不再受控制终端影响了 那么这个时候的话,他能够读取用户输入吗?他能够printf 输出吗?不行了

第3步:改变当前工作目录 chdir

改变当前的工作目录,那么我问一下大家,什么叫当前工作目录啊?我在这执行了sleep100

那么这个sleep100 执行起来以后,这个sleep100 当前工作目录是~/test/course/day8/2018082

.

就是这个进程是走哪个目录下启动的, 那那个目录就叫当前..目录,这个目录是可以改的, 怎么改?

man  chdir

他是不是只有一个参数? 这个参数是什么意思?是不是表示你要切换哪个目录?

在这给你说一下为什么要改变?举个例子 如果说你程序在u盘上,那么在u盘上,那么你插上电脑上启动起来之后,他的当前工作目录是不是就那个u盘啊?那么这个时候你把u盘拔掉呢?还能执行吗?执行不了了 你是不是得把你的程序放到不可插拔的这种磁盘上啊?是不是获得哪个区下 目录下?大家想一下哪个目录是肯定是不会插拔的?根目录下 是不是 home啊 这些东西肯定是不会插拔的吧?这一步是可以省略的,为什么可以省略啊?因为这个要看你往哪放了,你往u盘上放,那肯定得切换 你往这个我们系统的任何一个目录下,只要是不能插拔这一种,不能卸载 他都是可以的,这一步3是可选项 

第4步:重设文件掩码
mode & ~umask

为什么要重设源码? 文件源码是干什么的?是不是重载文件的时候计算文件权限的时候有一个文件源码吧?是不是有一个umask那个吧?还记得吗?那么最终生成文件还是生成这一个啊mode & ~umask ?

为什么重设文件源码呢? 在这我给大家说一下,大家首先知道文件源码的作用,这一个我举个例子,那么如果说我们这个守护进程 我们创建守护进程 我们这里面有创建文件操作,也有读写文件权限,读写文件的操作,如果说你这个文件源码是777  掩码是777是不是你根本就创建不了文件啊?即便你能创建文件,你能读写吗?因为你这个掩码你已经限制住了,你就没有读写权限了吧?

所以说你这个时候,你就从这个进程的话,是很受限于你父进程的文件吧?那么子进程的文件掩码是不是从父进程继承过来的?子进程的地址空间是不是从父进程复制过来的?父进程有什么,我子进程是不是也有什么啊?你可以做实验你试一试  文件掩码 重设文件掩码的主要目的是为了增加文件操作的灵活性,不再受限于父进程文件掩码的影响,最直接的例子 如果你 父进程的文件掩码是777,是不是表示没有任何权限啊?那么你说还有读写权限吗?

是不是就没有了?你这守护进程里面如果说对文件没有读写权限,你还操作文件干什么啊?是不是就没有任何意义了?要操作文件是不是必须得有读写权限啊?这肯定的吧?那最直接的例子 最直接的用法呢,umask(0000);就可以了 是不是这个比较暴力啊? 也就是说 我们不需要对文件做任何的修改,我创建出来,我指定的什么权限,是不是文件生成的就是什么权限?

mode & ~umask 这个umask(0000)是不是全1了?全1在和这个mode &操作是不是就是他自己啊?

这一步是不是必须的呢?这一步不是必须的,正常情况下,我们普通用户的文件掩码是多少呢?是不是002啊?

 第一个0 是八进制,后面这三个是不是就是那三组权限的意思啊?文件所属用户 文件所属组,和其他人吧?

第5步:关闭文件描述符 

这个是不是必须的呢?也不是必须的,因为咱们都知道 这个守护进程 是不是已经不再依赖于任何一个终端了?这个时候 0  1  2 是不是终端啊? 0 1 2  是标准输入 标准输出  标准错误输出吧?都是控制端, 还有用吗 第三个?没用了所以你得把他关掉,不关行不行?也没事,不关也没事,所以这一步也不是必须的,

关掉它呢 主要是为了节省资源, 那么我再问一下大家,我关掉这三个文件描述符以后,如果说我这个守护进程里面我再新打开一个文件,这个时候,内核给我们分配的文件描述符是几?是不是0,我是不是 给你说过了?内核给你分配文件描述符 是怎么分配的?是不是最小 且未被使用的啊?那你这三个都关闭了,是不是相当于没有再使用了,是不是内核就把0分配给你了?知道什么意思了吗?

第6步:执行核心工作守护进程的核心代码逻辑   这一步是不是必须的?这一步能省略吗?省略有啥用啊?这一步是核心操作,前面这些是不是都是套路啊?这个里面需要你务必掌握的,红色标记的是你务必掌握的,这些东西是必须要有的,其他的话呢,没有红色标记的可以省略

第 1步: fork 子进程,父进程退出

第 2步: 子进程调用 setsid 函数创建新会话

第6步: 执行核心工作守护进程的核心代码逻辑 I

如果说去掉那些不是必须的是不是有3步啊?

其中第6步:是不是我们真正的代码逻辑在这里面啊?

父进程怎么退出啊? 是不是return 就可以了,最简单的意思是不是fork出来以后,if pid>0 然后return 0 就可以了   是不是调用ext 也可以啊?是不是kill 自己杀自己也行啊?都行 父进程退出然后呢 目的我给大家说过了,我父进程创建一个子进程来,就能保证 这个子进程不是组长进程,为后续调用setsid函数提供了条件

要想调用setsid函数创建会话,这个进程 必须不能是组长进程,

子进程的他自己的id是不是就是会话的id?

 mode_t umask(mode_t  mask); mode_t 是一个八进制的一个数,地址填好就可以了

 

 参照步骤编写守护进程,

每隔2s钟 :使用setitier 函数设置时钟,该时钟发送的是SIGALRM信号,

信号操作: 注册信号处理函数,signal或者sigaction,还有一个信号处理函数

获取系统时间怎么获取,获取一次系统时间:time函数的  man  2 time

time_t time(time_t *tloc); 这个函数可以获得一个time_t 的一个值,这个值写到文件当中去, 是不是不好看啊?需要转一下 ,用 man ctime函数

这个ctime可以将刚刚这个time_t 转回一个字符串吧? 这个字符串你写道文件当中去是不是我们看着就比较舒服了?所以 还有一个ctime函数 

写入磁盘文件: 文件操作函数, open  write close 

 

 pid<0 整个退出 相当于我创建失败了肯定要退出 这没的说, 可以加个 pid>0 父进程是不是也要退出啊?

 我这可以写else吗?不写了吧?不写后面肯定是子进程吧?不写了

子进程 调用setsid函数创建会话 

setsid();

创建会话以后 子进程 既是组长也是会长 同时也拜托了控制终端的影响吧?

第三步 改变当前的工作目录

chdir("home/itcast/log");放在log下面也可以吧?不要乱指定,不要放在root文件下去, 是不是他有权限吧?

 改变文件掩码  你要知道为什么要改变文件掩码 目的是不是为了让子进程 操作文件灵活一些啊?不需要任何权限

umask(0000);最彻底来个这玩意  是不是这个 文件都有权限啊?

umask(0022);最常见的是这种

关闭标准输入,输出和错误输出文件描述符

你关闭像这个标准输入标准输出也好,你不要写0012 虽然说在我这个系统上也没事 但是呢 不标准,你写宏 你把这个文件描述符 移到 别的系统上 他不是一样的,但是这个宏是肯定一样的,

 那么我们的核心系统是什么啊?是不是没两秒钟获取一下系统时间,并且将这个时间写入文件啊?

咱们首先是不是要注册一个信号处理函数啊?然后调用settimer函数啊?这个问题知道

在这咱们用复杂的 

注册 信号处理函数 

struct sigaction act;

act.sa_handler 在这个文件处理函数干什么?是不是写文件操作啊?我先指定函数,这个函数 你可以随便指定吧?

act.sa_handler =myfunc;

act.sa_flags =0;

sigempyset(&act.sa_mask);

因为我们这个settime函数是不是发送这个sigalarm信号啊?我们用的是自然定时法吧?

第二个参数是act  第三个参数是不是原来的设置啊?直接设置为NULL就可以了

sigaction(SIGALRM, &act,NULL):

 接下来是不是我们要调用 settime这个函数了?你调用这个settime函数 这个函数的话 我们是不是要定义一个结构体啊?

这个记不住 过来查man setitimer  我们需要定义这样一个结构体吧?const struct itimervam

tm.it_interval.tv_sec  这个赋值是赋的什么值啊?是不是周期性 触发的值啊?这个值是几啊?每隔2秒执行,

 tm.it_interval.tv_usec =0;这个周期性赋值,还有第一次触发的时间吧?

tm.it_value.tv_sec = 3; 比如说我3秒钟过后可以吧?

tm.1t_value.tv usec =0;

你用的是哪种计数法吧?第二个参数呢?

setitimer(); 第一个参数  是不是 你用的是哪种定制法啊?

 在这里我们可以用which 我们是用这个ITIMER REAL 第二个参数&tm 第三个参数是先前的设置 NULL, 在这里你在写代码的时候我给大家提个小建议,什么建议呢?这个一个函数 是不是有很多个啊? 参数和参数之间 用空格分割一下,不要贴的太近了,你们看这个vis写代码的时候,他自动给你补上了?你写的时候呢?你这样写的话 我觉得美观一些啊 你该加空行加空行 

设置时钟,当然这个时钟是不是周期性的时钟啊?现在来说是不是我这个进程可以发送信号了?是不是可以发送了? 发送信号 我们在这是不是已经注册了信号处理函数了?那么这个时候是不是应该执行myfunc这个函数了?接下来你再写的话 ,该从哪写了? 是不是该写这了?是不是myfunc;

当然你这个进程不退出嘛?是不是进程不退出?while  你退出之后他是不是不发生任何信号了?

接下来咱们就 该写myfunc这个函数了吧?这个myfunc函数里面该写什么啊?是不是核心操作都在这里面啊?

是不是获取系统时间 然后写入文件啊? 还有时间吗?信号处理函数 是不是只有一个参数了?这个函数里面干什么?说你最直观的想法 干什么? 是不是首先得打开文件啊?因为你要写文件,直接打开文件,怎么打开  int fd =open()第一个参数是什么?是不是路径啊?是不是带路径也行 不带路径也行啊?我不带路径了,我这个文件创建在哪了?当前目录下,当前目录是哪?是不是你已经改了?如果你不改,那么你这个进程在哪执行创建在哪,你改了之后 他就给你创建了这个目录下来,第二个参数是不是打开方式啊?在这呢我们来一个全面的吧?可读可写 还用加别的参数吗?你得创建吧?还用追加吗?先不追加 咱们看看效果再说,一点点来,如果你不追加是不是意味着每次是不是都得覆盖啊?你这个文件是不是最多只有一行啊?待会咱们试一下 判断这个fd

如果这个fd<0怎么办? 那么你写perror顶事吗 在这?不顶事,因为你已经摆脱了控制终端的影响了,perror打印在哪打印?是不是perror在终端打印出来啊?在这直接写个return就可以了 后面能写数字吗? 不能写东西,先退出行不行啊?exit是不是可以啊?我们这么写,下面是不是打开文件成功了?打开文件成功你干什么呀?第二步 获取当前的系统时间 怎么获取?首先你要定义一个time_t类型的变量啊?这个你不够咱们可以查,是不是需要定义一个定时变量  然后把地址传进来,

那么这个函数调完以后呢,这个值 保存到这个变量里面来了,这个一定是什么参数?传入还是传出?是不是传出?或者你用返回值是不是也行啊?在这里 我们用这个就可以了,就不需要返回值了time_t *tloc  

那么接下来咱们是不是要去转换一下啊?把这个t 转换成 char类型,这样的话是不是我们就可以看了?

ctime(&t);

char *p = ctime(&t);

是不是他返回一个char* 啊? 

现在咱们是不是得到这个字符串了吧?接下来 是不是将这个char* 字符串是不是写入文件啊?写文件操作

write(fd,p,strlen(p));

完事之后要记得close(fd);  这个可以不写, 

这个操作完成了之后,咱们试一下看一下 我们第一这个守护进程是不是创建出来了?第二我们看这个文件内容到底是几行?按照我们刚刚推理是不是只有1行啊? 是不是每次你都覆盖啊?在一个 他是不是到底就摆脱了控制终端的影响了?其他 他只要是后台父进程 是不是就可以了?

或者是我们可以加上一句话,怎么加?我随便加一个 加这句话的目的你要知道 是不是看看他到底给不给终端输出吧?

 报错 落个字母👆

怎么没启动起来? 不要看表象,是不是在后台呢?怎么看? ajx观察一下,让你看第一个他的父进程 现在变成谁了?是不是1号进程啊?是不是正好符合预期啊?因为父进程已经退出了,他自己是不是相当于成为孤儿进程了?

 第二个 看看他的组长是谁啊? 2328 就是他自己的pid吧?组长id是不是还是他自己啊?会长是不是也是他自己啊?通过这个观察是不是正好 和我们讲过的一模一样?第二个 他是不是 也在后台执行啊?是不是也摆脱了 控制终端的影响了?

再看 文件,文件在哪呢?肯定没在这个目录下吧? 

是不是在这呢? 

怎么没内容呢?创建的时候 没有指定权限,后面是不是乱码了?

所以说我们需要加一个权限,所以说我把这个 给他干掉,怎么干掉啊?是不是只能是kill -9掉了吧?

 再把代码改一下,是不是后面加权限啊? 第三个参数吧? 0755

 我们这个用户 权限是不是满的?

后面的 其他的 咱就不管他了,ps -ajx 是不是也一样了?这个没问题,

 然后咱们看一下 这个文件有内容吗? 这个文件要删掉,不删掉不行,

 是不是一会就有了? 因为他每个2秒钟是不是会建一个啊?

 是不是有一行了?我教大家怎么去实时的观测 我教大家一个命令,

你看一下仔细观察他是不是变? 怎么变啊?是不是跟这个有关系 啊?就这一行,这个咱们怎么让他变成多行呢?是不是要打开文件的时候,我们要创建文件的时候 是不是要加o append 这个你知道就行了,再把它干掉 kill -9 2351

 

 第一个为甚少啊?第一个是上一次 的,看清楚 别被表项迷惑住了,后面是不是都是两秒钟一次啊?那么这个代码咱们就写到这

我们再给大家分析两点,有一个作业需要你去写一下,

 你的这个守护进程 你不要在你的系统上一直这样的跑,一直跑 跑一天的话是不是这个日志一定写很多啊?而且你可能也忘记关他了, 是不是?

第一个问题是这样的,我们是每两秒钟是不是要发送一个信号啊?那么这个信号处理函数里面 是不是每次都打开 关闭啊?很显然 这样做合理吗?不合理 这是第一个问题 那怎么解决?那这个文件打开 应该在哪打开啊? 那如果说我就让你在这里面打开  怎么打开啊?假如说 我举个例子,我第一次打开,我后来不打开了,怎么做?你在外面打开也行,如果说我就让你在这个函数里面打开你怎么打开?那么我问一下大家,我第一次打开,后面就不再打开,如何做到这一点?怎么做到?是不是用全局变量啊?加个标识 具体我先不告诉你怎么做的,你自己想一想 这一题并不难

第二点 我这个文件是不是越来越大啊?我如何做到 我让我这个文件不再写那么大?比如写一定的程度,我就给他改了 怎么弄?用哪个函数来获取文件大小?是不是statite啊  ?或者是lsice也可以啊?在这儿你用lsice是不是也不费事啊? 因为你打开文件了吗?是不是啊?这第二点 如何控制文件大小,如果日志文件.log 我只写到2k  举个例子啊,那么超过2k了呢?当然这个大小不一定完全是2k有的时候会稍微小一点 有时候稍微大一点,没有关系,就是说你的文件日志不能太大了,比如说我们规定2k 到2k之后呢  你怎么办?删掉不行,正常你的日志是不能删的,你们知道一般系统里面 一般银行里面是怎么做的吗?我给你再说一下 这一个进程 他一天 他有一个文件名字 他一天 后面是用户名,_2018.log每当这个文件 达到一定的大小之后呢,他就把这个文件保存起来了,他不删 这个文件他不能随便删的,保存起来了,你说他变成了.1  .2   .3 .4

当然你这个文件的值是mydeamon.log后续的是.1 .2   .3  .4 一般这么来,这个名字 你随便起

 咱们这个不做要求,总之 你要解决两个问题,第一个问题,

这是优化的东西,咱们目前来说 我们这个写守护进程是不是已完成了?这个案例写完了 下面我们来说优化的问题,

如何控制log文件大小,大家想一下,如果这个日志文件太大了,是不是我们看着很不方便啊?你得控制一下,那怎么控制呢?大致给你说一下,比如说 我这个 文件叫test.log 当这个test.log文件大小达到了1k了  那么你起个名叫test.log.1  再满了 叫test.log.2 就这个意思 这个名字你随便起,总之 你要解决这两个问题,优化的问题,留作大家的作业,同时你要把这个守护进程创建模型再熟悉一遍啊?

接下来再讲一下线程,

 先给你说这么几句 什么是线程?线程是轻量级的进程,在linux环境下本质上仍然是进程

在操作系统里面分配资源的最基本单位是什么 ? 那么系统调度的最基本单位呢?是线程 要搞清楚啊, 有同学就说了,前面咱们讲过的一些进程的话 也没有看到什么线程啊?如果这个进程里面没有任何线程,他自己就是一个线程,

进程:拥有独立的地址空间,拥有 PCB,相当于独居。

线程 拥有 PCB,但是没有独立的空间 多个线程共享进程空间,相当于合租

这个知道什么意思吗?我给大家发这个图 给大家解释一下,解释一下线程的基本概念

还从前面 大家熟悉的开始,咱们大家都熟悉进程空间吧?这一块我再给你画一下 看你掌握到什么程度了,

用户区里面有什么东西啊?

环境变量,命令行参数,动态库加载区  堆 栈 代码段(.txt),


.bss
.data 

受保护的区  一共是0-4k吧?

这个顺序不一定完全这样 但是这里面有这些东西,你别把顺序看成这样了,顺序不一定完全这样子,

内核区这里面有 PCB 还记的PCB里面有什么东西吗?是不是文件描述表啊?这个是我们的进程

那么线程和进程  到底有什么样的关系呢?咱们如何去创建一个线程呢?调用这个函数   原来创建进程用的是哪个函数?是fork 啊?现在用的不是fork了 用的是哪个啊?

pthread create这个函数后面再说,你要知道,调用这个么一个函数以后呢,在这个线程里面 就创建了一个子线程,这个创建子线程是怎么个过程呢?我再给你画这个意思,创建一个子线程  就成了这个样子了,进程空间并没有变化,他只是说多了个这玩意儿 又多出来一个PCB 首先第一个

你需要明白的是 创建一个子线程 系统并没有 重新再分配一个地址空间,那这个地址空间和原来的一模一样,但是呢 在内核区 他给你复制了一份PCB出来,说是复制的但是这里面并没有完全复制 比如说文件描述符,你这个线程呢?多个线程是可以共享一个文件描述符的,而不是说完全复制出来的 就是说我第一个呢,我调用这个pthread create 函数呢,创建一个子线程以后呢,首先我这个进程不再叫作进程了,叫主线程, 称呼变了 是不是本质没变啊?

那么这个PCB 当然这个得稍微标记一下,比如说我这个是创建出来的,这个你可以把他当成子线程的PCB吧?原来的是不是还是主线程啊? 当你创建出好几个子线程来,那么这个PCB是不是相当于复制好几份出来呀? 是不是又多了一份呀?再复制一份 再给你来一个,这些是不是相当于子线程啊?那么主线程是不是只有一个啊?那么怎么区分主线程和子线程呢?那么主线程是不是创建出来子线程啊?他们之间是不是可以理解为父子关系啊?可以这样理解,但是实际上 在内核当中每一个pcb都是独立的,那么在内核当中他看的时候呢,他怎么看?他只看pcb 那么这里有一个问题给问大家,那么你说这个我这个有多个子线程,他们是不是在一个进程空间里面啊?他有几个pid啊?切记 一个进程空间里面有1个pid,那有同学就有疑问了,那老师,我怎么区分这个线程和那个线程呢?每一个线程都有一个线程号,不同线程有不同的线程号,但是有相同的pid 那么你可以把这线程号理解成什么呀?理解成 一个一个线程他有一个编号,反正内核他能区分他们,你不然的话内核怎么区分呐? 内核调度的时候,调度你这个程序执行的时候呢,他是以PCB为单位的,那么也就是说以线程为单位,进行调度的,现在的时候,我这个空间里面现在有4个线程了,主线程有1个,子线程是不是有3个啊?这个时候呢,仔细看 成这个样子 了,现在是不是有4个线程啊?当然第一个是主线程,后面是不是都是子线程啊?

那么接下来 主线程和子线程呢,就类似于父子进程一样来一起抢cpu了,那么是谁先抢到 谁先执行?那么我问一下大家主线程 先创建出来子线程,那么是不是意味着主线程一定会先执行啊?未必吧?有可能呢,你创建出子线程以后呢,刚创建出来,然后失去cpu了,有没有可能?当然这个子线程得到cpu了,他是不是也可以执行啊?谁先执行 谁后执行 不一定,你就联想一下我们的父子进程就可以了,是一个道理,在一个你得知道,系统分配资源得最基本单位是什么?我这几个东西给你写到这,

1.系统分配资源的最基本单位 是进程

2.系统调度进程执行的最小单位是 :线程

3.多个子线程是不是和主线程共享 一个地址空间? 有一个pid

4.不同线程之间,是如何区分的呢?系统怎么区分我这个线程是这个,那个线程是那个,怎么区分的呢?是通过线程号来进行区分的,

5.这个里面我们多个子线程共享一个地址空间呢?哪些是可以共享的?哪些是不可以共享的?我举个例子 环境变量 能不能共享?能  命令行参数呢?  为什么栈不能?切记 栈是不能的,这里面除了栈以外,其他都可以共享,那么就有同学有疑问,为什么栈不行,那么我问一下大家,那么我这一个 每一个线程处理函数里面,现在没讲 线程是怎么执行的,每一个线程处理函数里面,他是不是很多都是要用到栈执行变量啊?如果你这个还能共享的话是不是就乱套了?栈空间是不能共享的,那不能共享的话,大概是怎么分配的,因为我们这个栈文件是不是大小是一定的? 栈空间大小是一定的,大概多大呀?是8k吧还是8m 用ulimit -a

 是8m

这个8m的栈空间够用吗?你申请这个栈内存的时候,你不能申请太大了,是不是他会说栈内存不够啊?那为什么说8m够用呀?因为你用完马上释放吧?是不是反复使用的?这个大家一般不用考虑这个问题,

除了栈空间以外,其余资源都可以共享

文件描述符能共享吗?能 举个例子 那必然说你这主线程打开一个文件,那么我这个子线程1 我也打开一个文件,我子线程1写数据了,我这个主线程能读出来吗?能不能?能,这一点有点类似于咱们这个是不是他们两个是同一个呀?当然是可以读出来的,

6.主线程和子线程 谁先执行呢?不一定,谁先抢到cpu谁先执行,所谓的cpu资源指的是cpu时间片,谁先抢到谁先执行,关于线程的概念 咱先说这么多,

多个线程和主线程 是共享 一个地址空间的,只有一个pid,一个pid是不是就意味着只有一个地址空间呀?那么这个是和父子进程是不一样的,父子进程之间是复制的啊?他这个呢是共享的,我们创建线程的这套函数,并不是系统调用,并不是系统调用,而是c语言的库函数,在linux下的c语言的库函数,但是创建进程呢,在内核当中 在底层实现里面,其实都调用了同一个函数, 叫什么?(kelaode)那么好 他可以区分,如果你创建的是进程  那么他就复制空间,如果你创建的是线程 他就共享空间,内部实现是这样的,

 讲义上也有一些其他的说法,咱们大致给大家说一说,这个图自己看,

这个线程呢,是最小的执行单位

进程是最小分配资源单位,可看成是只有一个线程的进程。如果说进程里面,没有任何的子线程那么就可以把这一个 可以把这个进程 看成一个线程,是不是只有一个主线程啊? 

特点自己读一读就可以了,

在内核中看进程和线程是一样的,内核认为进程和线程是一样的,他只看PCB吧?有几个pcb 他就认为有几个线程,当然对这个内核来说呢,他是不是都认为是进程,

进程可以蜕变成线程 我这一个主线程,我这一个进程创建一个子线程,最后我这个进程是不是可以称呼为主线程啊?叫法不一样,本质是不是一样的?

在 linux.下,线程最是小的执行单位;进程是最小的分配资源单位  这样的东西当作常识

 

 这个图是cpu调度的时候,现在cpu是不是都是多核的?那么多核呢?有的是多个cpu 有多核,那么这样你使用多线程操作起来就非常的快,为什么快呀?是不是真正的做到真正的并行 或者并发呀?

 怎么看这个线程的号呢, 通过一个命令 叫ps -Lf pid 

我这个给大家来演示一下,

打开火狐浏览器,你执行一下ps -Lf pid 看一下,这个pid 就是指的火狐浏览器的pid ,你执行一下看一下效果,那么能够看到一大串的 很多个线程,

实际上,无论是创建进程的fork,还是创建线程的 pthread create,底层实现都是调用同一个内核函数 clone 这个是不是都是他的底层实现啊?底层实现都是调用的clone 呀?

这个你最为了解内容就可以了,

Linux 内核是不区分进程和线程的,只在用户层面上进行区分。

 线程共享资源能够共享哪些东西?👆

 不能共享哪些东西?👆 线程是不是不一样的?每一个线程都有一个线程id呀? 

线程id和线程号是不一样的,线程id是给程序员看的,线程号是给内核看的,那个你不用管,我们自己编程的时候,我们用的都是线程id 接下来后面咱们讲编程的时候,线程编程的时候你就知道了

处理器现场和栈指针(内核栈) 这肯定不一样

errno.变量 一个进程里面有几个errno? 一个进程里面只有一个errno 那么这个errno 是不是相当于一个全局变量 啊?那么为什么说这个errno不能够在多个子线程里面一块来使用呢?为什么呀?如果你这样用就乱套了,我举个例子,这个是不是有好几个线程啊?你这个线程你如果出错了,是不是会把这个errno的值改了呀?他改了 他再读这个errno 还有效吗?他是不是也可以读啊?事实上是这样的,如果多个子线程读全局变量,那么这个全局变量是不是相当于共享资源啊?必须要加锁,所以呢我们一般也不用errno,用错误号,后面会给你讲这个的,在这个线程编程里面你就不要用perror打印错误原因了,应该使用strerror

这个函数可以把错误号对应的错误原因打印出来呀? 在不是线程编程里面你也可以用这个,只不过这个比这个perror稍微麻烦一些,

 信号屏蔽字 和调度优先级这些都不能共享,调度优先级这个肯定没得说吧?这个子线程里面是不是可以不一样啊?如果说你创建子线程了,没有进行任何的设置,这个优先级 大概都是一样的,但是你不能说他是共享的,因为可以改

 信号屏蔽字这个为什么不一样啊?这个是不是后来自己设置的啊?他能说他是一样的吗?一般情况下来说我们很少在线程里面使用信号来处理,因为他相对来说比较复杂,有的出了问题之后,你根本就找不着  这是前人总结出来的经验

还有的时候,你用的时候人家很多人都这么用, 你这么用应该是没问题 但是你注意一个函数或者 一种技术,你肯定不是第一个在用,已经是很多人用过了,而且他总结出来的经验,我这样做就好,不这样做就不好,你只要按照人家 方式来一般没问题,

线程优缺点,这个了解了解, 这个开销小,很容易理解吧,你创建一个进程是不是还得分配一个地址空间吧? 那我开辟多个线程就不一样了吧?我是不是给共享啊?很显然这个是很明显的,

数据通信、共享数据方便 

多进程通信方便还是多线程通信方便?因为你在一个地址空间里面嘛肯定可以共享,使用一个全局变量和堆内存是不是就可以共享了?

缺点:
库函数。不稳定  这个函数不是系统调用,而是库函数,那么库函数是不是意味着它里面封装了系统调用啊?调用pthread create 函数 内部他是不是调用的(kelao)啊?是不是fork内部也调用的(kelao)啊?等等

gdb,调试、编写困难 一般情况下我们使用线程编程的时候,我们使用gdb调试起来,相对来说麻烦一些,当然也没有人去使用gdb调试多线程编程 因为比较麻烦,

对信号支持不好 很少我们在线程编程里面使用信号 

优点相对突出,缺点均不是硬伤。 大不了不好使 我不用他 不就完了吗?而且每一种技术的存在 是不是都有它的技术场合啊? 一般我们在项目开发的时候,我们这么做的 至少我经历的是这样的,一般做业务处理数据库操作,很少有线程,

一般用数据通信 网络通信用线程来操作  应用处理呢?用进程来操作 一般是有它的使用场合的,

 怎么创建一个线程呢?调用这个函数 pthread create 函数  它的作用是创建一个线程 

它的原型是这样的,

首先第一个参数是 *thread,那么这个你猜一下是输入参数还是输出参数呢?每一个线程是不是都有一个线程id 啊?这个线程id 我们是怎么获取的呢?是在创建线程的时候呢,我们就获取了,

第二个参数是线程属性 const pthread attr t *attr, 比如说设置线程的分离属性,

第三个参数,void *(*start routine) (void *),是一个回调函数, 这个回调函数 参数是void * 那么这个void *就意味着你是否可以传递任何参数啊?整型 doub float  复杂的结构体是不是都可以呀?返回值是void *

第四个参数void *arg;这个参数就指的是回调函数的参数,你看他们俩(第三个参数)类型是不是一样的啊?这void *

返回值
成功,返回0    失败,返回错误号  这个错误号对应的错误原因你在这就不要使用perror打印了,要使用strerror 使用这个函数来打印就可以了,咱们在写代码的时候我会给你说

函数参数介绍,我就给大家说完了, 

如果任意一个线程调用了 exit 或 exitI 则整个进程的所有线程都终止,由于从main 函数 return 也相当于调用 exit,为了防止新创建的线程还没有得到执行就终止,我们在 main 函数 return 之前延时1秒,这只是一种权宜之计,即使主线程等待 1秒,内核也不一定会调度新创建的线程执行,下一节我们会看到更好的办法。

 还有一个注意点,你在一个线程里面你不要轻易的不要调用这两个函数,为什么呢?exit 或_exit 你这个一调用不得了 是不是把整个进程全退出了? 后面咱们会有专门使线程退出的函数,你说整个函数里面调用exit和调用return 有什么区别? 那么如果说你在这个函数里面调用return 他是这个函数的返回,并没有使进程退出啊?但是如果说你在这个函数里面,调用了exit 整个进程是不是都退出了?谁调用这个函数这个进程就退出了,他俩是有区别的,所以像这两个函数exit 或_exit 你要慎用 除非是你不得不退出进程,比如说(mailaoke)失败了,这个时候你还往下执行吗?直接就退出就可以了 这个介绍我就说这么多,下面咱们写个例子 给大家验证一下

当然你使用这个函数你得加一个头文件, 

 

 编译和 连接是不是得需要这个库文件啊? -l -opthread  

你就这么来用,你不加这个的话,他会说找不到这个函数的实现,这个是不是编译的时候才加这个啊?

首先定义一个变量来接收返回值int ret = pthread_create(); 

第一个参数是传过来的参数,在外面需要定义一个变量吧?需要一个pthread_t thread;

 第二个是一个属性,先不关心他 先传个null 表示我们 使用默认属性, 

第三个参数 这个能传空吗?这个不能吧?这个是线程处理函数吧? 线程执行函数  在这呢 我们起个名叫mythread ,

第四个参数 是不是一个参数的? 参数的 我先给他来一个NULL

判断一下 如果说这个ret  那么我们怎么知道,这个函数有没有调用成功啊? 看返回值 

 如果成功这个函数返回0吧?失败返回一个error number ,他并不是写errno那个errno吧?他们是不是写error number这个错误号啊?没有写那个errno全局变量啊

也就说如果调用失败的话这个*thread的值 是不是不确定的啊?这个值是哪个啊?是不是第一个参数啊?*thread 这个值是不确定的

所以说我可以这样写 !=0 就认为失败了

错误原因[%s]\n"  用strerror函数 打印 错误号是哪个?是ret

那么线程创建失败了  你还有必要往下走吗?return -1 就可以了

接下来 是不是开始写线程处理函数了?这个线程处理函数是什么类型的呀?是不是返回值是void*啊?

线程执行函数 和写线程处理函数一样 一个意思

 打印一下 chlid thread 获取线程的id 叫pthread_self() 这个函数没有参数 就这么写就可以了,这个函数类似于哪个函数? 是不是get pidf啊?是不是 类似于那个? 获取当前进程的pid是类似的,那么我看一下,我再给大家打印一下,我获取进程的pid 我看一下我这个父子进程pid是不是同一个吧?看一下我为什么这么干,是不是getpid()啊?

这个代码呢 我在这也复制一下,这个是不是主线程啊?27  

需要你观察的点 

一个点 你看一下这个pid 这两句话打印的pid是不是同一个值?

第二个 你观察一下他们的线程id是不是一样的?应该不一样吧?每一个线程都有一个唯一的pid 谁和谁都不一样,类似于人 的身份证吧?

如果我这样写,会不会有什么问题呀?有问题吗?如果我整个进程 空间被回收了,我子线程还能活着吗?他是不是就没有生存空间了,那么你看一下我们这个代码里面,我们这个man函数一直往下执行,是不是return0直接结束了?那么有没有可能他执行完 整个10-13 还没有拉起来呢?有没有可能?应该怎么办?是不是你应该让主线程晚一点退出啊,是不是写sleep 就可以了,你要知道写sleep 的目的是什么30,目的是为了让子线程 能够 执行起来  不加sleep 1 有可能我这主线程退出了 整个进程空间回收了,是不是子线程还没有执行起来呢,这样的话你就看不到运行的效果

 那你编译的时候怎么编译呀? gcc -o a.out pthread_create.c -lpthread

加个-lpthread 我们看这个帮助手册是不是看到了,这个库就是这个库-pthread

这叫库名,完成名是不是叫(libpthread.so)啊?

warning  我们打开pthread 什么类型啊?先不用管他 先看一下效果  

第一个你先观察pid是不是同一个? 

表明这个主线程和这个子线程确实是同一个进程空间里面啊? 第二个线程id是一样的吗?是不是不一样啊?这个值这么大 应该是打印一个ld吧? ld行吗?

 这个id 值是不是很大的呀? 这个pid是不是一样的啊? 通过这个案例的测试是不是明白一个道理啊?主线程和子线程确实是在同一个进程空间里面,他们的线程id是不一样的,这个案例呢我给你讲到这  能看明白吗?

直接复制一份,重点是传参数, 

咱们看一下怎么传参,传参的话比如说我先传一个基本数据类型,怎么传参?比如说int n =99;
我现在传一个参数进来,那么在这的话 你创建线程的时候,需要给这个mythread 函数传参啊?这怎么写?&n  22 是不是传一个地址就可以了? 那么这样的话你是不是得接收啊?这一个22 &n参数是不是传给 10 他的参数了? 他的参数是void *arg 你在这里是不是得接收啊?接收怎么接收?是不是你得写int *p = (int*)arg;  那么你这样在打印这个值的时候,如果说我这不是写这个*  我这么写呢?int n = (int) *arg;
这个怎么写 ?是不是前面得加个*啊?
int n = *(int) *arg;
(int) *arg;是不是去地址吧? 加个*是不是取值的意思?
咱们把这个值打印出来,看是不是这样的 13,带你出来只需要看是不是99就可以了吧?

 这个传参传的是基本数据类型,能不能传 复杂数据类型呢?
怎么传一个复杂数据类型?比如说我定义一个结构体,struct Test  9-13

接下来在主线程赋值啊?当然你这样的话没有结束符吧?最好给他初始化一下,

那接下来是不是可以传了? 接下来我是不是把这个t的地址值传进来呀?

接收怎么接收? struct Test *p 你可以写指针,写个指针 

那这样的话是不是把这个arg  void*是不是转换成这种struct Test 类型了?接下来后面是不是就可以打印了?不转不行,不行

复杂的结构体,传参, 

 

 创建很简单,就是在一个for循环里面吧?我们定义一个int n= 5;循环创建5可以吧? 这应该有一个数组吧?,

当然我们定义这个ret应该在外面吧?

 如何让子线程打印出来他是第几个来呢? 我们是不是应该给他传参啊?让子线程自己打印自己吗?

 在线程执行函数里面接收,12

 大家想一下,如果说这样写的话会发生什么样的问题呀?这个文件名太长了 我给他写个脚本来编译一下,vi pmak

$1第一个参数的意思,

 用怎么用呢?首先得加个权限,

这样就可以了 ,不要这个.c 

$1是不是就是pthread_create_loop这个意思?这个命令的第一个参数就是$1 那么$1.c是不是就是我们程序.c啊? 后面是不是得加个库名啊?-lpthread 生成什么呀?生成是不是这一个名字?pthread_create_loop

执行一下看一看效果,都是第五个,原意的话呢,如果你看过讲义的话,你应该清楚 为什么是都是5? 那么大家想一下,我如何让他不都是5呢?

现在先说第一个吧?为什么是5?

重新画一下图 分析一下

这是一个主线程 线程里面有一个i变量 初始值为0,我是不是循环创建5个子线程啊?其实现在我们这个里面是不是一共有6个线程啊?创建出第一个子线程的时候,我们是不是把这个i的地址传给这个子线程?那这5个子线程,那么我每次在创建的时候是不是我都把这个i的地址传给了这5个子线程啊?那这样的话,我可以这样说啊?这5个子线程共享一个地址, 能不能这样说?很显然正式这样的, 事实上也是这样的吧?那么再分析,首先你知道这个意思你知道了, 这5个子线程共享1个i,最后打印的是5,咱们看一下 为什么最后打印出的值的是5?这个怎么去分析?首先问第一个问题,这个i的值是由谁变的?谁把这个值改变了?谁改变的是不是主线程啊?是主线程把这个值从0变成了5,是不是这样的?到5的时候是不是循环创建了5个子线程已经结束了?就不再创建了吧?从0-4一共创建了5个

是主线程把他变成了5,那么大家想一下有没有这么一种可能发生?我这个主线程呢,我在一个cpu时间片内呢,我连续创建了5个,是不是?我连续创建5个子线程以后呢?这个i是不是变成5了?这个时候呢,每一个子线程才得到cpu时间片 才去执行,这个时候 很显然这个值就变成5了, 这个值 是不是这样来的?我稍做修改,就可以让这个值不是5,怎么改呢?这么改,你不是说一个cpu时间片内这几个子线程都创建完成了吗? 那我可以一个一个创建,我们试一下他的效果

 

现在是不是就变成 0  1 2 3 4 了? 原因是什么?

刚才我改了之后 是不是这种情况了? 那么我一开主线程 我创建一个子线程出来以后他是不是sleep1 啊?他sleep1的时候这个子线程1是不是得到这个cpu时间片了,他得到之后,是不是就把这个i打印出来了?很显然这个时候应该是几?0  第一个子线程肯定是这样的,那么同理 1  2  3  4 是不是也是这样的?但是这个问题你这样改 这个中间加一个sleep1 很显然不合理吧?那么应该怎么改? 如果说  我这个5个主线程 不再共享同一块地址内存空间,是不是这个问题就可以得到解决?是不是可以这样干啊?

如果说 我把这块i的内存空间 不再是一块了,如果说我是 每一个子线程,读一块内存 还会有这样的情况发生吗?看效果,这个比如说是数组,这个数组有几块内存啊? 是不是应该是5块?为此我们是不是应该定义一个 整型数组,这样的话,我再给子线程传地址的时候,第一次传第一个 第二次传第二个 第三次传第三个... 这样的话 是不是每一次  每个线程 他获得的这一块内存都是不一样的,那么这样的话,你无论怎么改 是不是只影响一个线程啊?能理解吧? 这个时候成这个样子了

是不是一个线程 对应一块内存啊? 

第一次 应该是0, 第二次1 ... 第0次到第四次 是不是一共5次啊?那这样的话 你每一个子线程在打印的时候呢,是不是他这一块内存对应的值是不会改变啊?

那么和这个图一是不一样的吧?这个值最后是5,

你首先第一个  你得明白 这个5 是怎么来的,由于i的内存 只有一块,那这一块内存 是被5个子线程共享的,那么这块空间 值的修改是不是被主线程来修改的?主线程在一个cpu 时间片内完成了5个子线程的创建 此时i的值为5,就是他创建完成以后,这其他的子线程是不是才执行起来呀?是不是才得到cpu时间片啊?而其他的子线程 在主线程让出cpu时间片后才得到执行,所以最后每个子线程读到的内存的值都是5,所以我们在每次创建子线程的时候加个sleep 那么就可以得到解决 但是你这样解决是不是不行啊?我们应该定义一个数组 让每一个子线程去访问每一块内存 ,那么这样的话, 5个子线程是不是互不相干啊?

那么改一下是不是这样的,为此我们需要定义一个数组 

pthread_t thread[5];

 你再观察一下,这5个子线程执行的时候,你看是不是 第一个子线程先执行,最后一个后执行?你也观察一下,

那么你看是第一个子线程先执行吗? 第一个子线程  反而后执行了,第四个是不是先执行啊?当然这个  每次和每次可能有所不一样,

现在是不是顺序变了? 这个是不是和服务子进程 谁先执行 谁后执行 道理是一样的,也就是说谁先抢到cpu时间片 谁先执行,

前面说过了,这个子线程 是不是不可以共享栈啊?为什么在这里能共享栈呢?这种共享是不是通过传递指针出去的,你说你在这个里面12 能不能用这个i啊22?如果你不传递指针进来的话,能用吗?这是第一个说法,第二个说法是这样的,其实并不是说栈 不能够被子线程所读取,或者共享,应该这样去理解,只要这块内存 他的生存期长 就行,

假如说我这样写呢?24-26

知道什么目的吗?这样写的目的是不是arr的生存周期是不是到这里就结束了26,到这结束了 这个是不是在栈上啊?这个栈 还能够被12读取吗?不能的 但是 如果咱把他去掉之后 这种情况,你说这个arr[5]这个数组的生命周期是不是和我这个进程一样啊?因为他是在main函数里面啊?只有我这个main函数结束之后,是不是这一块内存 他会被回收啊?

很显然这一块内存 可以被其他函数 或者子线程可以读取的,

前面咱们说不能共享,说的是什么呀?主要说这个里面,你每一个子线程里面 是不是他可以创建自己的变量啊?这里的变量12是不能够被其他的线程所读取的

你可以想一下 假如说你在这个里面定义了一个变量,你怎么让这个其他线程去读取啊?除非这一种方法?你在这里8是不是定义一个全局变量啊? 然后你用全局变量指针是不是指向这个啊?然后另外一个线程是不是也可以读取这一块内存啊?是不是也可以呀?但是是不提倡的,

只要这块内存的生存期 在读取的时候他是有效的合法的,那么你就可以访问,这个循环创建多个子线程 咱们就说这么多 这个图 这个原理 你要搞清楚 为什么说我一开始的时候呢 我连续创建了5个子线程 最后打印的都是5,原因 搞清楚,就是因为我多个子线程共享了同一块内存,而这一块内存的值最后被主线程改成5了?所以他在读取的时候读的肯定都是5,那么怎么修改呢?我可以让每一个子线程 访问不同的内存空间,那么这样 的话呢,是不是每一个线程 是不是 访问它自己的这块内存啊?那么这样的话 这5个子线程 访问内存还有冲突吗?是不是没有冲突了?是不是相互独立的啊?很显然这样的话呢,就能够解决问题 你不要在那里面加个sleep1就说解决问题了 那不是解决问题的根本方法,

这个循环创建多个子线程 咱们就说这么多

 

 

 

 2.6 pthread exit 函数 

 在线程中禁止调用 ext函数除非是你这个线程里面你调用函数的时候呢,确实发生了很大的错误,比如说malloc 内存失败,是不是这种情况下,你必须要是你的线程退出吧?而且应该也使整个进程退出,像这种情况下 比如说(kao掉),取而代之的是调用pthread exitt函数,这个函数只是说是一个线程 退出,并不影响其他线程,那么我主线程调用这个函数  会不会使整个进程退出呢?也不会,这个是指的是,只使一个线程退出 并不会使进程退出,再问 如果说我这个进程里面 只有一个线程,当然就是它自己,主线程,也就是说你这个一个进程 你调用这个pthread exitt函数的话 一个进程里面,你 调用这个函数 的话,一个进程里面调用这个函数,是不是可以使进程退出啊?这个函数是使一个线程退出,如果主线程调用 pthread_exit函数也不会使整个进程退出,不影响其他线程的执行

参数是  retval 表示线程退出状态,通常传 NULL, 如果你不关心的话,你就传一个NULL就可以了,但是这个值void *retval  这个地址 一定是 必须是malloc申请出来的,或者是全局的,也就是说它的生命周期不能在栈上,栈上的这一块内存是不是这个函数结束之后就被回收了?

我先给你写一个简单的例子 ,给你说一个简单的,先调用这个函数void pthread_exit(void *retval)  使一个子线程退出 ,这也很简单,

 咱们先在子线程调用一下可以吧?

这样写13就可以了 ,我就一个线程,

 你看一下就是说这个子线程调用这个13函数以后呢,那么它自己 这个主线程看一看它是否会受到到影响,

 ps -ef

死了没有同学们 ? 是不是主线程还活着呢? 我给大家演示另一个效果,有一个命令是不是ps -Lf

你们用火狐, 我把这个东西给你用一下, 我们这里是不是有一个循环创建子线程啊?这里面我可以让,知道什么意思吗?我先sleep(100)一下 40  这儿是不是也得sleep一下啊?

我让你关注一下线程号,怎么看? 我们怎么用ps命令来查看这一些线程,怎么看,

是不是跑起来了? 

 先看它的id是不是4574这个啊?然后ps -Lf 4574

仔细观察一下 这个是pid吧?pid这是5个子线程 是不是都是1个啊?ppid是不是父进程吧?当然这5个子线程 它的父进程肯定是同一个吧?其实也就是他的父进程吧?再看 LWP 这个就是线程号 第一个主线程的线程号 是不是就是他自己啊  pid 啊?其他的话呢,是不是挨个往下走啊?这几个肯定是不一样,那么对于这个linux操作系统来说 他其实找到是这个LWP 东西 这个是不是可以区分每一个线程啊?那么你用这个pid他能区分吗?是不是不一样?

再看这个NLWP 这个是线程的个数,那么咱们这个子线程是不是5个啊?主线程一个 一共6个,

STAT sl+    , l(aidou)空闲的意思 仔细看 我给大家发过一个 ps命令的详解吧,

TTY pts/19这个终端是什么意思 ?是不是就这个终端啊?👇

 也就是说你可以使用 ps -Lf  4574 这个命令  然后这个4574是进程的pid吧?来查看一下你这一个进程里面到底有几个线程吧?是不是就6个吧?一个主线程 5个子线程,

看这个代码里面,我们这个代码里面,我们是不是刚才调用了这个函数了?pthread_exit(NULL);
是不是使一个子线程退出了?那么如果说我这个函数呢?在一个主线程里面调用会有什么样的效果呢?我在13加一个sleep  这句话我在主线程调用,那么我就看一下什么呢?我看一下这个主线程调用这个退出之后,这个子线程还有没有活着,如果死了,是不是意味着 整个进程结束了?如果还活着是不是不影响其他子线程啊?

咱们看一下是不是这样的?

 怎么查呀同学们?是不是先查pid啊?

是不是有僵尸线程啊?你看是不是  你看这两个是不是全变成僵尸线程了?其中整个pid 4600 这个是不是主线程啊?主线程是不是已经退出了?是不是退出了?我问一下大家 为什么说这个主线程现在变成了僵尸线程了?可以这样想一下 这个问题啊?那么这个僵尸 其实这个主线程 相当于这个主线程,是不是相当于原来的进程啊?你说这个进程到底退出了没有啊?退出没有?进程是没有退出的,为什么呀?因为你还有一个子线程还活着呢,有一个子线程还活着呢,是不是就意味着这个整个这个进程是没有退出的,整个进程是没有退出 是不是就意味着应该是没有回收呢,你这个主线程退出了,但是呢你这个主线程的空间是没有被回收的,为什么呀?因为你这个整个的进程是不是还活着呢?还存在的呢,只有当最后一个线程也退出了那么整个进程空间才能被回收,那么这样的话才能消除僵尸进程啊?这个怎么解决?

接下来讲到这个了,如何避免这种僵尸线程的产生?

刚才我们那个测试是不是在主线程里面调用那个pthread_exit这个函数了?之前我们都看到了,那么主线程退出 并没有使整个进程退出吧?

那个子线程是不是还活着呢?那么现在咱们 如何  就是说 如果说这个子线程退出之后呢,这个子线程 退出之后 ,他并不能回收自己的资源 ,他自己 是不能回收的,那么应该调用一个函数,应该让主线程调用一个函数 去回收他,哪个函数呢? pthread join 函数 那么这个函数相当于进程里面哪个函数?是不是wait?给那个是一个意思,好

函数描述: 阻塞等待线程退出,获取够程退出状态。其作用,对应进程中的 waitpid()函数。

waitpid这个函数 是不是在  父进程里面调用的?那么你这个函数 pthread join 在哪调用啊?在主线程里面,你不要调返了

int pthread join(pthread t thread ,void **retval);  这个函数 第一个是pthread t thread 什么意思? 你回收是不是得知道回收哪一个啊?这个就表示的是你想回收哪一个子线程,第二个是一个二级指针 我问一下大家 这个二级指针 你用的时候 你怎么用啊?你说这个我先说这个retval  是传入还是传出啊?你现在是不是想后去线程退出状态啊?你很显然你这个返回值你肯定获取不了啊。返回值只是成功失败的意思,那么很显然 你应该什么呀?是不是 这个应该作为一个void **retval 传出

那么我用的时候你应该怎么用呢? 一般我们先定义一个void*  一级指针,然后把这个一级指针地址传给他就可以 ,

这个指针指向内存 正是void pthread exit(void *retva)  正是这块内存retva 他俩之间的联系是不是建立起来了? 我待会验证一下是不是 我是不是可以定义一个变量 然后传递到这void pthread exit(void *retva)  然后 我再调用int pthread join(pthread t thread ,void **retval); 是不是获取那个状态呀?然后我把这两个地址打印出来,看看他是不是同一个 是同一个 那就意味着 这个指针 pthread join 里的retva指向的地址是不是就这个 pthread exit 里的retva啊? 是不是可以这样验证啊?

咱们来试一下是不是这样的,

这个函数的作用就是回收子线程, 

这个函数 pthread join指针指向的内存 就是pthread exit 的函数 他的参数, 大家试想一下 我问一下大家一个问题,那么你说  我在这个子线程里面 既然说我这个 pthread join 找个参数指向的内存 就是这个pthread exit函数指向的参数, 那么问一下 那么这个pthread exit函数 能不能是一个栈上面的内存啊?为什么不能?释放掉之后是不是就可以晚了?你再想访问是不是就不能访问了?

什么意思呢?比如说 定义一个变量 int a =9;pthread_exit(&a); 这里是不是应该传一个地址值进来呀?你这样传行不行啊?是不是不行啊?有些同学可能试了一下 偶尔行,偶尔行 你也别这么干,那么这样的话 我是不是应该给你去掉了32?如果说我这样写 我还用sleep10吗33

 回收子线程,用哪个函数?第一个参数 就是thread  你要回收哪一个子线程啊? 目前来说  我这里是不是只有一个啊?就传找个进来就可以了? 第二个是不是一个指针啊?指针呢,是不是你应该定义一个void * p 然后你是不是要把这个p的地址传进来?这样的话 这个是不是&p才是二级指针啊?能理解吗?咱们看一下这个p的值到底是几?很显然我刚才这么写是不是不合理呀? 因为那个是栈内存 那个函数结束之后,那块内存是不是就被回收了?所以你打印那个值 搞不好 程序会报错,我把这个地址给你打印出来,看看这个地址和那个地址是不是同一个地址 地址直接写p %p是打印地址的

*p是不是打印p指向内存的值的?

 这个是不是打印p的这块内存 啊? %p 是不是打印内存的意思?

上面的话呢,咱们把这个a的地址打印一下,看看这两个地址是不是一样的?

你这 一开始 是不是先获得这一块内存啊? 下面应该是 强制类型转换啊?得转一下不转不行,

下面这个p是不是指向了 13这样一块内存啊? 这样应该是不行,你首先发现 这两个地址是不是同一个地址啊?

退出状态时0,这个好像不对, 为什么不对呢?有没有可能这一块内存13 被回收掉了?你打印地址可以打印出来,但是你用这里面的值 是不是有可能不一样了吧?所以这里面的值 我们应该用什么?全局变量? 或者堆 是不是也可以呀?

 

现在是不是对了?那么你看一下  刚才那个 地址是不是很大呀? 这个是不是比较短啊?你表明他们肯定不在同一个区域吧?

 这个是不是在全局数据区呀?刚刚那个是在栈上啊?  这个地址是不是也是一样的?[0x601070] 很显然我pthread_join 这个函数 第二个参数指向的那块内存,就是你退出的时候 这个15参数地址吧?这个理解了没有?这个是给你用了一个简单的 数据类型,如果说这一个函数他的退出状态是一个结构体呢?能不能是结构体?他这个参数是void*吧?

咱们看一下 我定义一个结构体 struct  Test  9-13来个复杂点的,接下来是不是我们应该定义一个全局变量啊? 这也是一样的,定义一个全局的,struct Test t;

赋值的话 在哪赋值?是不是你得在线程里面赋值啊? 全局 对你不格式化也行,

那么这样的话咱们 是不是要给他传参啊?👆

那么你这边在这个pthread_join是不是也得强制类型转换啊?怎么转换?47 struct Test *pt =是不是指针指向那块内存啊?是不是再转一下啊?是不是和我们上午讲的那个类似?(structural Test *)p

这个涉及到一个类型转换的问题,特别注意的是什么呀?我得把这个说清楚,你这个得知道 这个pthread_join 他的第二个参数是不是&p啊?他指向是不是&t这个值?通过咱们刚刚打印的地址是不是打印出来地址是是不是看到效果了?

线程回收 线程分离, 刚才咱们用的是pthread_join是不是回收子线程啊?

 咱们可以完全不这样干,可以设置线程为分离状态,默认情况下 这个子线程是非分离的,我们可以设置 这个线程 为分离状态,这样的话呢,那么这个子线程 在退出的时候呢,他能就自己回收自己了,这样就不关子线程的事了,那也用不着主线程去调用pthread_join进行回收了,如果说我这个子线程已经是设置为分离属性了,也就是说它退出去之后 是不是它自己可以回收自己啊?如果在进程里面 子进程里面也可以这样的话,就不会有僵尸进程产生了吧?那么这个一般用于什么情况下呢?一般用于网络服务居多 ,什么意思呢?你想一下 网络服务是不是会处理大量的交易啊?比如说有一个客户端连接,那么它就可以创建一个子线程, 让子线程去处理这个连接,处理这个业务,那么如果说你这个主线程呢,你只是用来 等待子线程退出,很显然是不合理的,因为什么?因为pthread_detach是不是阻塞函数啊?它是阻塞函数,那么很显然这样做不合理,特别是有大量线程,频繁的创建,频繁的销毁,这种情况下呢,我们不应该让子线程 仍然是非分离状态, 应该把他设置为分离状态, 那么这样的话呢,这个子线程退出的时候,它就可以自己回收自己

就是这么一个意思, 那么如果说子线程它已经是分离状态了,那么你这个主线程里面你再调用pthread_detach再回收的话呢,他就立刻返回了,它就不再阻塞了,知道什么意思吗?默认是不是我们这个pthread_detach会阻塞啊?这个咱们看一下怎么用,pthread_detach这个函数就可以设置线程为分离属性,

咱们就写一个例子 给大家试一下吧,如果你这个 这个线程终止之后呢,这个状态呢,是不是由它的父进程回收啊? 这个咱们试过了,那么如果你设置为 detach 状态之后呢,那么你这个线程一旦终止  就由它的 它就自己回收自己了,而不保留其终止状态。那么也就是像资源是不是在他退出之前已经被销毁了?已经被释放了?不能对一个已经处于 detach 状态的线程调用pthread join  言外之意 只要你这个子线程 已经处于分离状态了,那么在这个主线程里面你再调用pthread join函数,那么它也回收不了,他会立刻返回 是不是,如果说它是非分离的,那么你再调用pthread join 函数是不是主线程会阻塞啊?那么现在 我就给大家来写一下这个,这个很简单,

设置子线程为 分离 属性

设置分离属性以后呢,那么也就不再让它的主线程去回收它了,在哪设置呀?

设置线程为分离属性我们如何验证子线程 它是分离的呢?我们可以调用 pthread_join 来测试一下 ,正常情况下,如果说 你这个子线程是非分离的 默认情况下就是非分离的 那么这个时候呢 你调用pthread_detach它会阻塞,如果说是分离的呢?它就不再阻塞 立刻返回,意思明白了没有?
判断一下 如果ret!=0;这个pthread_join 函数返回值是这样的吗? 是不是成功返回0 ,失败返回一个错误号吧?

 

 咱们看一下这句话能不能打印出来35,因为正常情况下 这一个是不是会阻塞啊?因为你把这个线程设置为分离属性了,那么这个函数 就不再阻塞了,

 是不是这个函数报错了?实际上也就是说你这一块 你这个子线程的资源是不是全部被回收了?当然包括它的id是不是也被回收了?这个时候你再调用这个函数呢,它就不再阻塞了吧?是不是立刻返回啊? 我们可以验证一下

 

 

 我把这个函数注掉30,会有什么效果?这个函数 他会阻塞吧?怎么验证啊?我是不是可以加一个sleep啊?加这个sleep的目的是什么?是不是看他到底阻塞不阻塞吧?要搞清楚他的原因,阻塞在什么位置了?是不是这个函数阻塞了34?10秒钟过后会打印出来吗37这句话,是不是没打印出来呀?这个就是你加这个和不加这个的区别,看出来效果了吗?

 设置线程为分离属性,当然后面还有一个方法可以设置线程分离属性,刚才我们这种方法是不是在线程创建成功之后设置的,还有一个呢,大家记得我们这个pthread_create这个函数吗?这个创建子线程的函数 第二个参数 是不是也是线程属性的意思啊?我们还可以在这设置,这种方法是在创建线程的时候呢,去设置的,刚才我们用的这个呢,是在创建线程成功之后,设置的,

 

总结一下 
线程相关函数 
1.创建子线程用 pthread_create
2.线程退出呢 这个线程退出呢,并不是子线程退出,是不是主线程也可以调用啊?调用pthread exit 这个函数

3.回收子线程 pthread_join
4.设置子线程为分离属性 pthread detach
 

 

 

 接下来讲 取消线程 pthread cancel 函数 这个函数 有点类似于进程中的kill 是不是相当于给一个线程发送一个信号啊?有点类似于这一个,咱们看一下,这个函数的原型 叫int pthread cancel(pthread t thread); 只有一个参数吧?  thread 是不是表示要给哪个线程发送信号的意思?或者叫取消某一个线程的意思吧,成功返回零 失败返回错误号 大家注意 今天 包括明天讲的一系列的线程相关函数成功都是返回0,失败都是返回错误号,
线程取消他不是实时的,线程要想真正取消呢? 内部必须有一个取消点,那么什么叫取消点呢? 你也可以把他理解成检查点,那么如果说你不知道什么是取消点,你可以想像一下这个问题,调用什么东西可以进入到内核当中去啊?我们的程序在运行的时候,调用哪些函数可以进入到内核啊?一定是系统调用,read函数能不能进去,write sleep 是不是都可以呀?一定是这样的,还有就是 只要这个函数 是阻塞的,阻塞的函数也是可以进入内核的,
sleep是不是可以阻塞啊?read write是不是都是可以阻塞的啊?

 

 那么如果说你还记不住,那么没有关系,你也不用记了,有一个函数可以设置取消点,
void pthread testcancel(void); 这个可以设置取消点,那么为什么刚才说线程的取消并不是时时的呢?大家想一下 我举个例子 这是我们的代码吧?这是我们子线程代码 ,执行执行执行... 代码很长 而且这个里面没有一个取消点,没有一个取消点,那么你说这一个子线程收到了这个取消的命令,之后 他能够退出吗?退出不了,或者你这样理解, 比如说前面100行代码没有取消点,到101行 有取消点,但是我这个线程在执行第一句话的时候,他就收到了,退出信号,他是不是一直前面都不退出啊?到取消点之后他就退出了,是不是我们就可以说这段时间是有延时的,我们一会试一下,如果没有取消点是不是真正的就不能退出,加上取消点之后呢,他就能退出了,咱们可以实验一下,咱们写一个什么样的例子呢?
写一个小例子,这个取消干什么用啊? 你在这个进程里面调用kill函数 干什么用啊?你别光说杀死啊?杀死只是其中的一个功能,

是不是可以发信号啊?是不是让这个线程执行某些动作啊?这一个在我们这个线程里面你调用
pthread cancel主要是取消的意思,主要是让他退出,这是他的主要目的,这个怎么改呢? 我们可以这样改,我问一下大家这个printf里面有没有取消点啊?为什么有啊?因为这个printf内部是不是调用write了?是不是可以进入内核啊?只要是能够进入内核的都有取消点,这句话是不是得去掉,不去掉是不是坏事了? 我来个这个 while 1

 int a ;  int b ;随便写 但是你不能些printf 你些printf就坏事了,我随便定义这么两句话,那么这样的话我这个while1循环是不是一直是死循环啊?我让这个主线程 去杀死他,我写sleep(100)写长一点 35,这儿可以不这么写,我是不是写一个pthread_join就可以了,pthread_join是不是保证我这个主线程之后退出啊?那我给他发 我调用什么呀? 我取消他,取消子线程,怎么调用啊pthread_cance1(thread); 注意看,我是怎么写的,那么你看一下到底有没有退出,咱们怎么看他有没有退出啊?

 你怎么知道他结束没结束啊?你是看不到的,是不是你查一个 ps -Sf 就看出来了?联系起来,

执行之后 是不是起来了?

 然后咱们看一下,是不是有这一个啊? 然后看ps -Lf 4786 几个?是不是两个?

 子线程是不是还活着呢? 刚才我是不是取消了?35 我是不是发了这个信号过去了?他是不是还活着呢?为什么还活着呢?就是因为你这个里面没有取消点12-17 那么我们如何设置一个取消点呢?
这个时候就用到了pthread_testcance():这个函数,这个函数没有实质性的动作 只是在这里打了一个标签,相当于打了一个标签,那么这个只要是这个线程收到了这个退出信号之后呢,他看到这个标签,这个取消点之后呢,他立刻就退出了,他是干这个用的,不干任何其他的动作,咱们可以试一下是不是这样的,注意看 ,我设置一个取消点,pthread_testcance():这个函数有没有参数?设置取消点 你现在 我把这个添完之后 再执行,ps -Lf看看 那个子线程还有没有活着 是不是就看出来了?

 是不是结束了?还活着没?是不是死掉了?那么这个函数是不是 这个子线程死掉之后 他是不是立刻就返回了?pthread_join(thread,NULL); 能看到效果吗?如果这个子线程没死的话,这个pthread_join是不是一直阻塞啊?看到这个效果了吗?你不信你可以看一下,如果说你不写pthread_testcance() 不写这个,试试printf 看看这个行不行?这样的话 你是不是会打印大量的 这个应该很快就完了,因为他会立刻就取消啊?这个但是这个20 应该是会打印出来一些来,当然也有可能一个不打印,有没有可能?正好比如说,他发过去了40,他刚刚跑起来20  他 一看到这个取消点是不是马上退出啊?也是有可能的,跑起来 你看他是不是打了一个啊?打了一个就死掉了

 这个取消点的意思明白了没有?取消点,我这儿给你写一句,其实我们这个讲义上是不是也有啊?线程是否有取消点,并按请求进行执行动作的一个位置通常是一些系统调用creat,open,pause,close,read,write.... 执行命 man 7 pthreads 可以查看具备这些取消点的系统调用列表。

 这个里面列出的这些 全部都是有取消点的,你看这些

是不是都是系统调用啊?或者是内部调用了系统函数吧?这个你不用记,如果是你不知道,到底有没有 那很简单,是不是调个函数就可以了?这个咱们就说这么多

 这个函数比较两个线程id是否相等

他的参数是不是有两个啊?一个t1 t2,说白了就是比较一下t1 t2是否相等啊?当然呢,在我们现在呢,我们根本就用不着他,为甚呢?因为我们的线程id是不是整型值啊?是不是你用=就可以了?这样做的目的,为什么也提供了这样的一个接口呢?他是为了以后的扩展来使用的,那现在的话呢 你是一个整型,那么以后万一是一个复杂的结构体呢?有没有可能啊?他给你预留了一个接口,只是目前我们用不着,这一个我给大家试一下,很简单

 

 可以看一下 看看他的返回值,这是有一个现成的吧?thread 再比较一下主线程的,这个主线程的和他的这个是不是肯定不一样啊?主线程的线程id是怎么获取的啊?是不是也是pthread_self()这个函数 在哪调用,那么就是获取的哪个线程的id吧?
看一下他的返回值 

 是不是我们认为不等于0就相等啊?等于0就不相等啊?
第一个参数 先来个子线程的  再来个自己的 如果说!=0 是不是相等?

 

 是不是不相等啊?咱们来个相等的,是不是把这个改一下就可以了?他自己肯定相等 是不是?这个函数目前来说没什么用,你知道有这回事就可以了, 
当然让你用的话你是不是直接写等号是不是也可以啊?

把这个进程的函数和线程做一个比较,

fork类似于pthread create 一个是创建进程,一个是创建子线程,
exit退出一个进程, pthread_exit是使一个线程退出

wait/waitpid 回收子进程,

pthread join I 回收子线程,当然这有比咱们这个比子进程 有一个高级的地方在哪呢?他是不是可以设置分类属性啊?设置完分类属性之后,是不是他是不是自己可以回收自己啊?如果说子进程有这样的一个功能 就肯定不会有僵尸进程产生了吧?

kill这个是不是给一个进程发送信号的意思啊? 专业的说法,不是杀死的意思,是给一个进程发送信号的意思,
pthread cancel 那么这个线程里面用的是不是pthread cancel 啊?是取消一个线程 ,后面这个getpid获取的是进程的pid吧?那么在子线程里面可以用pthread_slf 这个函数来获取子线程的id,大家注意 这个线程id和线程号不是一回事啊,线程号是给谁用的?线程号是给内核用的,线程id是给程序员用的,是不是我们自己用啊?我们写程序的时候呢,在代码里面我们都是用什么呀?都是用threadid 线程id 他是不一样的,

 咱们在哪给大家提过一句啊?在创建子线程的时候,是不是第二个参数里面有一个指针啊?那个就是设置线程属性的,那么linux 下线程的属性是可以根据实际项目需要,进行设置,

 

看一下这个函数怎么用,这个你自己读一下,默认情况下是不是非分离的?你要像把他设置为分离状态呢,一共有两种方法,第一种方法是在创建线程成功之后,是不是在后面调用pthread_(tachi)() 那么一种方法呢是在创建线程的时候呢你就给他指定好,表示这个线程是分离属性,怎么设置呢?看一下这个步骤,相对来说很固定,你就这么干就可以了,
第一步先定义一个属性类型的一个变量,pthread_attr_t attr;这个类型的变量,_t在哪我给大家说过吧?什么意思啊?type_defan 定义出来的吧?其实他这样定义的意思呢其实就给你隐藏了具体的实现细节吧?
第二个呢?进行对这个线程属性 变量进行初始化,也很固定叫int pthread attr init () 他的参数是不是就你上面刚刚定义的attr这个指针啊?把这个地址传进来就可以了

第三步呢?
设置线程为分离属性
第一个参数很明显就是上面这个啊?把地址传进来就可以了吧?后面这个detachstate是一个int类型的一个变量,这个一共有两种取值 用两个宏来表示

PTHREAD CREATE DETACHED(分离)
PTHREAD CREATE JOINABLE (非分离)
你肯定你得指定分离吧?你要指定非分离的话是不是就不调这个了,那就没意义了,是不是,
那么第四步,那么接下来是不是你要调用pthread create 函数创建线程啊,其实呢前面这几步都是在为这个函数的第二个参数做准备的?
那么这个子线程 如果说子线程不用了,退出了,那么在主线程里面你记得要用
int pthread attr destroy(pthread attr t *attr);这个函数 进行资源的释放,当然这个参数是不是就是这个attr啊?
是不是就是这个变量地址传进来就可以了,

很简单,就是一个固定的步骤,这个东西记不住 用记吗?
知道怎么查 你得知道有这么一回事,
接下来给你写一下这个例子,

 把这个步骤给你用一下,
在创建子线程的时候,设置 分离属性

第一步先定义一个变量, pthread_attr_t类型的变量

定义完之后,初始化attr变量
这个函数都是以pthread_attr开始的吧?他是不是只有一个参数啊?

第三步 设置 attr为分离属性啊?到这为止是不是还没有创建子线程啊?

设置attr为分离属性

pthread_attr_setdetachstate 其实这个单词 你只要读对了是不是就写出来了?
第一个参数是什么?attr啊?第二个参数是个宏,PTHREAD_CREATE_DETACHED 
第四步 接下来是不是全有了?那么这个属性设置好了是不是把这个NULL换成什么啊?&attr
那么其实你看一下,前面这几步是不是一直在为这个&attr做准备的啊?
现在这个是不是完事了?你记得这个不用的时候记得干什么?记得释放  释放线程属性,释放资源的意思,怎么调用pthread_attr_destroy(&attr);
是不是完事了? 那么你设置完之后,

这个属性,怎么验证一下这个,到底是分离 还是非分离的,看他阻塞不阻塞,

验证子线程,是否为分离属性
ret = pthread_join(thread,NULL)验证一下这个

ret !=0 是不是失败了?

打印日志的时候要打印清楚,起码你要自己看的明白,

看看我们这个样板  上面,我加一个sleep2 在上面我加sleep2的目的是什么啊?
如果你这个线程是分离属性,这个是不是会立刻返回啊?40-45  如果这个是非分离属性,这个是不是会阻塞?而且如果是分离属性之后呢,这句话是不是应该打印出来啊?44

我们验证一下是不是这样的,

 他是不是立刻返回了?也就是说 其实我们设置线程的分离属性是不是已经成功了?
因为你只要设置线程为分离属性以后 你再调用pthread_join函数呢,这个函数就不再阻塞了,反过来,如果这个线程是非分离属性呢,这个函数会阻塞吗?是不是会阻塞啊? 这一块咱们就说这么多

总结两句,
这个是不是应该在创建线程之前啊?
设置什么呀?设置 其实说之前不太妥当  是不是 的时候啊?
设置线程属性,为分离属性,第一步先干什么啊? 是不是定义变量啊?
1 pthread attr t attr;
2 pthread attr init(&attr);
第三步接下来是不是要进行设置吧? 
3 pthread attr setdetachstate(&attr,PTHREAD) 
最后不用了

4.pthread attr destroy(&attr);
这个步骤是很固定的,你记不住也无所谓,到时候你查的时候 知道怎么查就可以了,
但是你得知道两种方法,一个是在创建线程之后,设置线程为分离属性,一种是在这个创建线程时候吧?

当然你创建线程时候呢,前面这几步你是不是必须得有啊?你得给他干什么呀?当然我们这时候
pthread_create(&thread);第一个参数是不是一个输出参数啊?第二个参数是不是&attr 第三个参数随便写一个mythread
最后一个NULL 是不是这样的?
第五步不用了,最后不使用了记得销毁吧?

 

 最后一个专题,线程不同步,这个也是明天 咱们也是主要讲的一个线程同步的一个概念,在这呢先给大家接触接触

什么叫线程同步,线程同步,指一个线程发出某一功能调用时,在没有得到结果之前,该调用不返回。
同时其它线程为保证数据一致性,不能调用该功能。
我举个例子, 这个去银行取款机取款,那么那个取款机会门上有个锁啊? 你进去之后那个锁是不是锁上了?
那么试想一下如果那个没有锁的话你直接进去了,一个歹徒呢,

手持砍斧直接进去了,把钱拿出来,那么是不是我可以这么说啊?当然你那个时候你可能还没把钱拿出来,你正想准备输入密码呢,他进去了,是不是相对于要打断这个取款操作啊?然后他拿着砍斧 你一边去,我先取 这是一种情况,
另外一种情况 他等着你把这个款取出来之后呢 他再威胁你把这个钱给他,

这个意思是这样的就是说如果说你去取款机去取款了,你在没有完成操作取款之前呢,别人呢是不能进去的,别人不能去取款,为什么呢?因为那个插卡的地方 只能插一个卡呀?只有你把钱取了值后,卡抽出来之后,你打开门出来,别人是不是才能进去啊?那这个时候是一个什么操作啊?是不是有先有后一个互斥操作啊?你用的时候别人能用吗?是不是不能用啊?别人用的时候你能用吗?是不是也不能用啊?这个在咱们生活中例子非常的常见,

你举一个,是不是上洗手间也是这样的
创建两个子线程,让两个线程共享一个全局变量int number,那么这个子线程可以共享这个成员变量啊?然后让每个子线程 数5000次数,看最后打印出这个 number 值是多少?
这个题目的要求 理解了没有?看一下
你这两个子线程是不是有一个成员变量啊?初始值为0,然后我线程a我数5000次,我线程b再数5000次,那么你说这个number的值最后是多少?
正常情况下应该是1万,有的时候可能不是1万 咱们先把程序写出来观察一个现象,

而且有时候呢 你一想打出来,不是这样的还不好打印出来,
试很多次才能试用出来,我把这个程序写出来之后呢,咱们看一下是不是这样的,然后现象我们得到了,咱们分析一下为什么不是1万 是不是我们通过现象分析原因啊?那么找到原因之后,是不是我们接下来要去解决它呀?
咱们看一下是不是这样的?
 

咱们看一下 大家把这个例子举一下 
咱们是不是要接下来创建两个子线程啊?
这个也很简单嘛,咱们就在这里创建就可以了
注意看,我让大家看这个现象呢,那这个函数的话呢,咱们也给他分一下吧mythread1 10-13来个1,那么我问一下大家,这两个子线程 能不能执行相同的函数啊?是可以的,我这给你分开之后呢,是让你看的清楚一些

 

 

 这是不是线程处理函数呢?那这个怎么写呢?我们是不是应该定义全局变量啊?
int number = 0;那么接下来我们是不是应该数数了?
定义一个宏 NUM 这个5000还比较小,有时候还不能看出效果来,咱们打印一下,咱们数数的时候可以这样数,我呢定义一个中间变量 int n;
然后呢我先这样干 n= number; n++; number =n;现在同学们还不知道为什么要这样写呢,原因能想出来吗?是不是你看过这个讲义你应该知道,

 这两个函数是不是操作执行完全一样啊?所以说呢,这两个应该是同一个线程回调函数的,这个是不是写mythread2 现在是不是两个线程执行函数了?

 怎么看一下是不是,咱们打印一下,最后在哪打印呀?56

 是不是这一个?但是呢 你是不是应该这样啊?  是不是我应该回收子线程啊?你如果不这样写的话是不是就立刻就退出了?

 这样的话是不是肯定会保证主线程后退出啊?咱们试一下是不是这样的,但是你这样的话呢 这个太快了,咱们给你看一下效果啊,如果你不在这个子线程里面打印printf的话呢29-34,他这瞬间就完成了,咱们先看一下效果,

 这个执行快慢也取决于你的电脑,

 这样测不出来效果,应该怎么办呢?
我们可以在这个里面打印printf 这呢我可以写个什么呀? 线程1,我打印个编号,

 

 你说你打印printf 是不是涉及到io操作了?涉及到io操作是不是要慢一些啊? 这个你要清楚原因,为什么要这样干,你不然的话呢,你很快就完事了,看不出来效果,当然你举的这个例子呢,有的时候呢这个效果不是非常明显,

 

 这个肯定会少一次,有的时候少的不止一次,少了没?为什么少了? 这个现在你先看他的现象,大家注意不是百分之百的少,找个电脑慢的容易敲出来太快的不容易敲出来,注意点,你看一下,这些是不是好多的2输出来的?当然还多也有1数出来的,从这个数字2 或者1 你能想到什么呀?是不是你这个是交替执行的,线程1和线程2是交替执行的?有的时候是线程1有的时候是线程2,这里面是不是又涉及到cpu时间片的问题了?这个现象大家也看到了,确实是大家在数的时候呢,我们发现一次 少四吧?是不是9996啊?少了4次,为什么少4次?你先想一想,你先琢磨琢磨,这个呢先说到这,现象咱们已经看到了,为什么少四次呢?课间你先想一想,这样咱们改成 大家先试一试吧,#define NUM 20000 
这种不是每次都会出现,概率呢还是有一定的概率呢,只不过比较低一些而已,

 少了吧?效果出来了,现在咱们分析一下 为什么会少,其实呢,这个东西你只要理解了,那个cpu时间片呢,这个东西迎刃而解,
两个线程数问题分析
其实这个就是什么啊?这个涉及到线程同步了,我们完全可以采用手段 让他不出现这个问题,首先这个num 是不是全局变量啊?num在地址空间里面是不是一个全局变量, 我再问一下大家一个问题,我先不往下画呢,那么你说我在这个代码里面我为什么这么写?我为什么分了好几句 这么写?

 我为什么不num 直接++啊?

这个是不是我们有两个线程啊?
注意看效果,线程1 再来一个线程2,看一下这个 这样咱们不关主线程的事了,咱们只看子线程 线程1 线程2 是不是都是子线程啊?
那么在数数的时候呢,我们是这么数的吧?我们这个代码呢,给你抠出来,
一共4句代码,在这里为什么我要加这个n中间这个中间变量?我是为了模拟什么?开始的时候这个n的值是多少?是0吧?我举个例子比如说咱是从100开始,当然前面数100都数对了,有可能吧?接下来从100开始,那么这个线程1呢,那么他数100的时候呢,他是不是先n =number;啊?此时n是多少?
n是100 n++ 是101

当准备执行第三句话 number=n的时候 突然发生情况了, 让出了cpu时间片,这个cpu时间片 大家这个概念现在是不是慢慢清晰了?注意看 也就是说 当执行到n++的时候呢,这个线程1呢,让出了cpu时间片,有没有可能?接下来 让出cpu时间片以后呢,线程2 开始取值,我把这个也给你扒出来,复制出来就可以了,

中间这个线我就给他擦掉了,我就给你讲一个少的情况,其他情况你就明白什么意思了,
线程1 线程2 当然他们的代码是一模一样吧?这个线程1呢,执行到n++的时候呢,是不是现在变成101了?是不是做++操作已完成了,接下来他失去cpu时间片了,是不是让出cpu的控制权了,接下来线程2 得到cpu时间片,他继续从这个number取值 是多少?是不是还是100呀?接下来n++操作 变成101  好,这一步也完成了多少?
这个现在这个number 是不是也变成101了?接下来打印是不是也是101啊?(线程2)
重点是 这个值已经变了,这个值(方框里面的)是不是由100变成多少?变成了101  接下来 当然 在正常情况下,这个数数是不是要数很多次啊?是不是同学们?没有关系 这个多少次也没有关系,咱们就给你讲一种,最简单的一种情况 你就知道什么意思了,
接下来他数完了(线程2)
接下来他(线程1)又得到cpu的时间片了,那么你说他从哪开始执行啊?

是不是从number=n;(线程1)开始执行? 此时他 自己里面的n他知道吗?多少?因为这个n是一个什么变量啊?是一个栈上内存变量吧,是他自己的变量吧?这个n不是全局的 这个number才是全局的, 搞清楚
接下来他是不是要赋值了?赋值之后,变成多少?101 这个值打出之后是多少?101
其实这部操作number=n的时候呢,是不是要往内存写啊?这步操作是往内存写(方框里面的) 这个number 变成多少?
是不是这一步number=n的意思是不是把这个n的值赋到number内存当中去啊?变成101啊?是不是会 把刚才那个101给覆盖啊?

这样的话是不是你线程1和线程2 数了两次数,但是实际上这个number 只加了一次 ,那么这样的话 他是不是就丢了一次?这就是我们那个数据少数的原因,
当然我给你举的是不是一个特例啊?
那么你大不了 你这个...你是不是多循环几次啊?多循环以后是不是这儿也仍然会被覆盖一次啊?(方框里面的)
当然如果那样的话 会少的更多了,你比如说线程1数了一半了,突然 这失去cpu 这儿还是从101开始数,
是不是 ,这样的话是不是就少很多啊?

这种情况就是由于cpu时间片来回的切换照成的,我之所以给大家把这个代码给你分成好几句 那么就是为了 是不是为了让cpu时间片 在这个线程1和线程2之间切换的概率提高啊?

如果呢我就写一句,就printf number++ 是不是也可以呀?一句是不是也可以搞定呀?那这样的话 这种概率就非常低了,是不是相对来说比较低呀?当然也不是说就肯定是出现不了,只是说那种现象很难出现,即便是我们这样的话,是不是我们执行很多次才发现一两次啊?这个意思搞清楚了没有?为什么说会少?清楚了吗?
 

描述一下现象

线程1 得到cpu时间片,在执行到
他得到cpu时间片吧?在执行第2句代码以后,失去了cpu的时间片,也就是实际上呢,这个++操作是不是还没有写到内存啊?那么写到内存的操作是不是第三步啊?
number =n是不是才会写到内存当中去啊?
失去cpu时间片,然后线程2 得到cpu时间片,从内存取number的值是100,当然这个初始值是100,然后线程2执行完1-4句代码之后,是不是相当于这个内存当中的值 number 值已经变了?此时number的值 在内存中是101,接着看,也失去cpu了,
线程2 失去cpu时间片后,线程1获得cpu时间片,
此时这个线程1应该从哪执行啊?
此时这个线程1从中断位置,中断位置是多少?该执行第3行了,从中断的位置开始执行(从第3句代码开始执行)
第三句代码是不是要赋值啊? 是不是 number =n; n是多少 此时number的值变成101,会将刚刚线程2的值覆盖,所以呢造成 数了2次数,但是只加了1,按理说数两次 是不是应该加2啊? 就这种情况造成的,那这个描述自己可以尝试写一写,然后呢,重点是你要理解,这个到底是怎么回事,

为什么少了?主要是由于什么呀?主要就是由于在线程1 线程2,之间来回的切换cpu时间片造成的,那么试想一下,如果说我线程1的这4句话,全部执行完毕的话,全部执行完以后呢,再切换到线程2,他有这种情况发生吗?反过来是不是线程2 也是这样的?是不是?
所以说就用到了咱们马上要给大家说的互斥操作,那么什么叫互斥操作呢?互斥操作是这样的 那我线程1 我执行的时候呢 我只要没执行完,你线程2 不要执行,
那我线程2 在执行的时候呢? 我没有执行完你不要执行,意思明白了没有? 就类似于什么呀?你们去取款机取款,你没有取完之后呢,你别把门打开,你取出来之后你,取完钱之后,你出来之后呢,们开了 另外一个人是不是就可以进去了,同理他是不是也要加锁呀?

 他要锁门,那么后面的人能进去吗?是不是也进不去呀?就这个意思,那么怎么解决这个问题

呢?

 看讲义,看这个图,

刚刚这个原因我给大家分析了,一个是有共享资源吧?在我们这个里面哪一个是共享资源啊?是不是number啊?number是一个全局变量吧?
第二个是随机调度 随机调度是不是指的是cpu来回切换啊?

第三个线程间缺乏必要的同步机制。也就是说,就是因为你这个线程1线程2 他们来回的切换 也就是说我线程1没有执行完呢,线程2就开始执行了,有可能你线程2 还没有执行呢,我线程1就开始执行了,

 是不是他们俩缺乏必要的沟通机制啊?如果有这种机制 这种情况就不会产生了,接下来我给大家说的是,应该采取什么样的机制?问题是不是找到了?分析出来了?接下来你要找一下 该怎么解决,咱们讲这个时候呢,应该是先看现象,现象发生之后呢,先分析原因,原因找到了 接下来提出解决方案, 那么思路是这样的

先看一下这个图,T1和T2 是不是表示两个线程啊?那么这个共享资源呢?在我们那个例子当中就是number吧?当线程T1 在访问共享资源的时候呢,他先尝试着去加一把锁,现在先不要慌这个锁是什么的,你只是说有这么一回事就可以了,他先尝试加锁,如果说这个锁没人占用,就类似于什么呀?取款机这个门是打开的,是不是你直接进去就可以了?你进去之后,你先干什么?是不是你先锁上啊?他先尝试加锁,如果说这个锁没有被占用,他先加锁,然后读取共享资源,也就是说对共享资源进行操作,如果说这个操作完成了,他是不是必须解锁,就像什么?你在取款机上取款的话,你取完款了 你在这里面呆着,别人能进去吗?除非有这种可能 外面下雨了你不出来,是不是,或者外面你看到有豹子的情况你也不敢出来,等等... 你不出来,别人是进不去,这个也是一样的,我限制机呢,我加锁完之后,我操作共享资源完以后呢,我必须解锁,我解锁的时候,这个线程2是不是就可以尝试去加锁了?线程2 在加锁的时候呢,如果说这个锁 被占用着呢,他会怎么样?是不是等待?那就相当于什么呀?你在取款的时候呢,如果说有一个人在进去了,把锁锁住了,然后这个人在取款呢 你能进去 吗?

如果你能进去的话,一会儿 110就过来了,是不是?你有这个作案的嫌疑,这个动机,所以说这个T2 访问共享资源的时候呢 如果说这个共享资源,目前 这个锁是被占用着的,他就阻塞等待,那么什么时候他就可以进去了呢?是不是线程把这个锁释放了他是不是就可以进去了?他进去之后,他干什么呀?他是不是加锁啊?他有加锁,他加锁成功之后呢,他就叫读取共享资源的数据,或者叫操作共享资源,那么操作共享资源完了之后,他是不是也要释放锁啊?就是这么一种机制,

 你就联想一下 洗手间这个门,或者联想一下这个取款机,最直接的例子 你T1用的时候,你T2不能用,我T2用的时候,你T1不能用,总之这个读取或叫操作共享资源数据的时候呢,只能有一个线程在访问,同时的话有两个线程吧?是不是没有?同时不能有两个线程在访问

number是一个共享资源,是一个全局变量,那么堆的话是不是也可以做为共享资源啊?操作共享资源,操作完之后呢,必须解锁

操作资源和共享资源是同一回事,属于互斥操作  他能用 他就不能用,是不是两个人只能有一个人啊?他们两不能同时使用,

 

 是不是刚刚给大家说过了,我们要使用锁啊?那么这个图当中这把锁怎么来表示,

 那么要使用锁呢,首先你要定义一个pthread mutex t 类型 这么一个类型的变量,
比如说我们先定义一个变量,
pthread mutex t mutex;这个mutex的取值呢,你可以这样去理解,他的值只有两种值,1、0是不是意味着 打开关闭?
第二步 进行初始化,你光定义一个变量呢,第二步还需要初始化,叫pthread mutex init 
那么这个初始化的第一个参数 很显然就是上一步你刚定义的那把锁啊mutex?
第二个是一个属性,设为null就可以了,一般我们使用默认的互斥锁就行,

restrict 关键字 这里面给你用的是restrict 用于限制指针的使用,
看看第三个,这个锁,咱们现在锁也有了,初始化也有了,接下来呢,是不是你要使用这块锁了?那么在哪使用?
你想一下在哪使用?是不是你应该在访问共享资源的位置开始使用啊?只要出现了共享资源 是不是要加锁啊?是不是这样的?你要知道加锁的位置,不要加错了,
待会我们代码会给你说一下,你比如说像我们这个代码里面我 我给大家看一下,在我们这个代码里面 你说在哪加?
加在共享资源出现的位置,那么哪一个是共享资源啊?是不是nuber啊?

是不是加在这四句话的上下啊?上面是加锁,下面是解锁,那么你只是线程1加锁解锁,线程2不加行不行?没用了是不是,相当于这个线程2没有限制了是不是?也就是说他线程1得到锁的时候呢,
线程2是不是在加锁的位置要阻塞啊?

 最后这个锁不用了要记得释放 int pthread mutex  destroy

 mutex一互斥锁变量  后面咱们还有锁呢 叫读写锁,

加锁是怎么加呢?一个叫pthread mutex lock 函数 很显然这个函数,一看就知道是加锁的意思吧?那么你可以理解成mutex-- 初始值是1,表示锁可用--变成0了,是不是可以理解为锁不可用了?
这个锁被占用着呢,那么锁用完之后要记得pthread mutex unlock 函数 是这个进程加锁的一定要记得解锁,否则的话呢,会出现什么情况?是不是别人进不去了?你想一下你去取款的时候,你取完款你不出来,你在这找个板凳你在这乘凉 歇着,别人能进去吗?是不是进不去呀?跟这个道理是一样的,那么这个解锁就类似于mutex++ 大家注意了实际上不是这样做的,你只是这么理解就可以了 那么这个mutex++ 之后 是不是由0变成1了?
1表示锁可用,那么可用是不是别人可以获得锁呀?

 这儿还有一个pthread mutex trylock 函数 是尝试加锁的意思, 当然这一个函数是一个阻塞函数,如果说加锁不成功,那就意味着这把锁是不是被别人占用着呢?被别人占用着这个函数就阻塞等待,int pthread mutex lock  直到别人释放锁,

 pthread mutex trylock 函数 这个函数是尝试加锁,他阻塞吗?不阻塞,如果加锁不成功他立刻返回,他有返回值,你可以通过返回值判断一下 他到底有没有判断成功,是不是也只有一个参数啊?

现在我给大家把这个代码加上 咱们尝试一下,加锁之后这个效率变低了还是变高了?变低了,你加锁之后 能并行吗?是不是只能有一个线程在执行啊?你线程1执行的时候你线程2能执行吗?你线程a释放锁的时候 你线程b是不是才能获得锁啊?他们俩是不是交替执行啊?你要搞清这个东西,这个才是本质的,

第一步先干什么?使用这一个属性的时候,限制属性是不是先定义一个属性变量啊?第二步初始化,第三步是不是使用啊?
这一个也是类似的,
第一步先干什么?定义一把锁 你是不是得有锁啊?
那么你说这个锁是不是应该是一个全局变量啊?为什么呀?因为你这把锁 是不是要在线程1和线程2 里面都使用他啊?你肯定必须定义一个成员变量,不要搞混了,怎么定义啊?


第一步:定义一把互斥锁,
pthread_mutex_t mutex;
这个初始化应该放在哪写啊? 能放在线程里面写吗?
不能 这个锁的初始化功能 只能放在主线程里面去写,你子线程里面做的工作是不是只是加锁和解锁的操作啊?
而且你这个锁的初始化,应该放在创建线程之前,为什么呀?不然的话,你这个有可能线程拉起来,你这个锁还没有初始化成功呢,有没有可能啊? 应该放在之前,

那个属性是不是attr init啊?这个是mutex_init 第一个参数是怎么?是不是他只有一个参数啊?他有属性设为null就可以了,初始化完成之后呢,如果你不使用了,释放锁

 

这个是不是定义一把锁,初始化也有了,接下来是不是开始加锁了,加锁在哪加呢? 我给大家说过了,在共享资源出现的位置,共享资源出现的位置在哪呢?
是不是这个number就是共享资源啊?是不是应该加在这四句代码的上下啊?

你加完锁之后,你代码的效率一定变低了,这没得说,因为现在是不是肯定不能并行了吧?是不是只能一个一个的来呀?

试一下代码好不好使,

 

 应该加一个time 来计算一下时间,
时间不用说,肯定是变大了,大家能分析出来吗?
我把这个锁去掉,咱们一个加锁 一个不加锁,
这里我问一下大家,你加锁的时候 加的不是同一把锁 好使吗?没用吧 是不是没用?就像你这个取款机有两个门,俩门俩钥匙,你开门进去了,是不是另外一个人也能开门进去呀?他俩必须是同一个门,同一把钥匙吧?

 

 

 一起测可能测不出来效果,咱们分开测,这个与咱们的实际想法完全不一致,不太合理,按照咱们的分析肯定是慢,这个不用说, 刚才加锁的是9秒多,不加锁是9秒多,
原因大家知道吧,就是线程1和线程2 来回切换cpu,造成这种情况,那么这个实验我给大家讲完了,我给大家讲一个概念,

原子操作的概念 原子操作是什么意思呢?原子操作的意思是这样的,如果说操作没有完成呢,他不放行 这个操作 他要么完成 要么不完成,只有两种结果,那么大家看一下,我们的代码,
我给你分析分析有没有这种情况出现,那么我这个代码是不是这个加锁解锁是不是这一步啊?有没有可能 我这个代码执行到这之后24  25 ,失去cpu了,那么失去cpu之后,那么线程2能执行吗?为什么执行不了啊?因为他是不是得不到这把锁啊?那么也就是说这个操作 有可能会发生这种情况,第一种情况,我这个加锁和解锁这个操作有可能是多个cpu时间片来完成的,第二种情况,有可能是一个cpu时间片 我执行了好多次,
当然你中间这个代码量很大的话23-28,那肯定是 有可能在多个cpu时间片之内才能完成一个操作,那么你这个代码量 如果是很小的话呢,有可能是1个cpu时间片呢,重复很多次 加锁减锁 他同样也存在,执行到在这个代码某一个位置的时候,他失去cpu时间片了,肯定也是存在的,但是他失去cpu时间片以后呢,线程2 仍然得不到执行,原因就是他没有释放锁呢,
他呢25虽然失去cpu时间片,他得到38cpu时间片了,他一看呢,他得不到锁怎么办?他是不是在这阻塞啊?他阻塞等待,那么他阻塞等待呢,然后阻塞期间他又失去cpu了,是不是25他又继续执行啊?执行到这之后呢28,他释放锁了
线程2 他是不是才有可能加锁成功啊?那么也就是说 我们是使用了这把锁来模拟了一个原子操作,那么试问一下,我们这个操作里面是不是每次我只要一加锁成功了 肯定是这几句话24-27能够执行完了?

只要你加锁成功了,那么23-27这几句话肯定能执行完,绝对不存在 你加锁成功了 
然后线程2这里面又执行一半,他去执行了,这种是绝不可能的,为什么绝不可能?
因为线程1执行不完他就不放锁嘛,他不放锁 那别人能执行吗?我是不是说这几句话23-28,要么你不执行,是不是你加锁不成功就不执行啊?要么你执行完,你加锁成功是不是肯定能执行完啊?
所以这种就是一个原子操作,
关于这个原子操作的提法 说法
在数据库当中也存在,你现在先理解原子操作的概念,现在对原子操作概念有没有一个基本的认识啊?

 

 这个操作 22-28 要么不做,要做就做成 不存在 一个做成,做了一半,成功了一半的情况这种情况是绝无可能的,
这是原子操作
我是这样理解,给你说了,原子操作指的是该操作要么不做,要么就完成,不存在,执行了一半 成功了一半,失败的情况,这种情况是不存在的,
该操作  要么不做不执行 要么就完成,是不是要做完啊?要做就做完其实我们是使用互斥锁来模拟的一种原子操作吧?是不是模拟的?实际上这个 真正的原子操作 ,底层的原子操作是这样的,原子操作是什么? 你这几句话23-27 是一次性完成,但是在我们这你说他会一次性完成吗?是不是有可能会执行完一半失去cpu了?这有可能,那么真正的原子操作是n++的操作,n++的操作他本身就是一个原子操作,他绝对不会出现n++这个操作还没有完成呢,他就失去cpu了,这个东西是绝对不存在的,知道什么意思吧?因为我们每一个操作是不是都是cpu指令啊?是不是最后翻译成cpu指令啊?你这一个操作有可能,比如说n++ 有可能有好几个cpu指令才完成,但是呢他已经把一些操作给你设定为原子操作了,这个操作要么就一次性完成,要么就一点一点来做,就这么一回事,那么对于我们这来说,是不是我们这些 23-28 我们中间是不是有可能会中断啊?为什么说是模拟呀?他实际上不是完完全全的原子操作,只是说模拟的一个原子操作吧?
反正我能做到这一点23-28 我要做做成了,要么我一点没做,反正我做到这一点了,那么关于原子操作的概念呢 ,我给大家也一块来说一下,那么这个加锁操作 咱们就说这么多

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值