多进程 , 多线程 是编写一个多任务程序的相关的机制.
什么是多任务程序呢 ?
一个程序能够同时做多件事情. 比如说:QQ 登录以后同时和多个人聊天还可以上传下载,听音乐 就是多任务.
程序是存放在磁盘上的,那么我们的CPU实际上是不能直接去访问磁盘,要执行一个程序首先是要把程序中的内容,指令和数据.都加载到内存中这样我CPU才能去执行指令,去访问数据所以要执行一个程序,就要为它分配内存,除了内存之外还要分配什么?分配CPU资源
对于CPU来讲,系统中很多进程都需要用到CPU所以每个进程你还要为它分配时间片? 这些资源的总称我们叫进程
进程创建好以后,里面的数据是能够改变的,而且进程还有不同的状态,需要创建,调度,执行,消亡动态的过程.每一次执行程序的时候系统至少创建了一个进程来执行这个程序,
通过系统数据,我们的操作系统就能够有效的去管理进程,一个操作系统里面有很多的进程在运行,那么这些进程都需要我们的操作系统去为它分配资源去管理去回收,所以每个进程创建好以后都有一个系统数据段,里面存放的是一些进程的属性,通过这个系统数据段,我们的操作系统才能够管理进程.
系统数据段里主要包含 :进程控制块 \ CPU寄存器值 \ 堆栈
寄存器作用:
每个进程里面都会保存当前进程所用到的寄存器的值
PC:Program counter 程序计数器
存放的是进程下一条指令的地址. 下一条指令地址就是执行了一条指令以后从PC中取出下一条指令的地址.为什么要保存PC的值呢 ?
多任务分时操作系统,(单CPU情况)宏观是多任务,微观上它是一个顺序执行.假设我们只有一个CPU的话那么这个CPU在某一个时刻只能执行一条指令,它只能去执行某一个进程 ,不能同时执行多个进程,为什么宏观上是多任务并发呢?
因为每个进程都有时间片,大概几十毫秒,进程在执行的时候时间片会递减,当时间片用完以后,进程就会让出CPU,由CPU去执行其他的进程.在一段时间之内,若干秒之内我们的用户把他会感觉到多个进程多个程序都在运行.实际上是通过这个分时来实现多任务机制, Program counter 非常重要了,里面放的是进程的下一条指令地址, 当进程的时间片用完了它就会让出CPU.等到它下一次再一次被调度执行的时候很显然进程应当从上一次停止的地方继续往下运行,所以需要从Program counter中取出下一条指令的地址, 从下一条指令开始继续往下运行这是Program counter的作用.
系统数据段中还包含了堆栈 : 栈,我们的所有的局部变量都是在栈上创建的.也在栈上被自动释放, 我们程序编译好以后,程序中实际上并没有包含栈.也就是说我们所有的局部变量,并没有包含在我们的程序中.程序中只有一些全局的数据. 和一些静态的数据,那么所有的局部变量都是在进程创建的时候我们的操作系统创建进程栈.所有的局部变量,包括函数的参数 .返回值,都是在栈上被创建.所以栈很显然,如果没有栈的话,这个程序就无法运行.
一个进程里面 有代码,有数据,有系统数据.
子进程几乎就是把父进程的内容都复制了过来,包括父进程中的代码,指令,包括数据,比如:我创建的变量,父进程的一些系统数据,比如说:PC的值啊,栈里面的值等等都复制了过来, 还包括父进程打开的文件,子进程也会继承过来.
子进程几乎复制了父进程的所有内容. 为什么不是所有内容?
因为他们是两个就进程 .他们的PID 和PPID是不一样的.几乎复制了还是有些区别的
虽然说子进程复制了父进程几乎所有的内容. 但是父子进程,不光是父子进程,linux系统里面所有的进程每一个进程都有自己独立的地址空间都有自己独立的内存. 是相互不影响的. 虽然父子进程中有相同名称的变量,但是这两个变量它所对应的物理内存是不一样的.所以无论你是在父进程中. 还是在子进程中,改变变量,无论是全局变量还是局部变量,对另外一个进程都没有任何影响.
父子进程 有自己独立的地址空间, 有自己独立的 代码和数据. 这样说是方便我们去理解.
有可能父进程先结束,也有可能子进程先结束.那么如果
父进程先结束:
1.子进程成为孤儿进程,被init进程收养
系统会把孤儿进程统一由init进程收养.init进程很特殊,它是我们linux里面内核启动以后创建的第一个用户态进程.进程号是1,
为什么要被系统的init进程收养呢 ?
系统有规定, 每一个进程结束的时候,必须由其他父进程来回收.,
一个进程在他的生命期内, 进程号是不会变的,但是进程的父进程号是会有可能发生改变的.
2.子进程自动变成后台进程运行,
shell又重新变成前台进程
子进程先结束:
1.父进程如果没有及时回收, 子进程变成僵尸进程
僵尸进程 :当 进程结束的时候,进程的相关资源都已经被释放了,但是进程的PCB没有被释放. PCB里面放的是进程的返回值 ,以及结束方式,而这些信息我们的系统规定必须由父进程来回收来处理 . 所以当一个进程子进程结束的时候,如果它的父进程没有及时的去回收子进程的这些返回值和退出状态.那么子进程的这个PCB就不能被释放.那么这时候我们就把这个子进程叫僵尸进程. 在编写多进程程序的时候特别是服务器端那么一定要避免僵尸进程的出现.
1.子进程创建好以后 ,子进程是从什么地方执行呢 ?(父进程是从main函数开始执行)
当创建好子进程以后, 父进程肯定是继续往下执行.
子进程是从fork的下一条语句,下一条指令开始执行.为什么是这样?
首先子进程实际上他从父进程里继承了几乎所有的内容, 包括父进程的PC(程序计数器,里面放的是下一条指令的地址)的值.当父进程执行fork的时候,PC里面存放的是fork函数里面下一条指令地址, 子进程创建的时候复制了过去. 所以子进程创建好应当是从fork的下一条语句, 下一条指令,开始执行., 所以注意, 子进程并没有执行fork
所以子进程创建好以后, 是在fork下一条语句之后执行的
2.子进程创建好以后, 父子进程谁先执行?
对于linux系统来说的话,并没有规定一定是父进程先执行还是子进程先执行,这的看内核的调度, 如果内核调父进程了,就先执行父进程,如果内核调子进程了,那就先执行子进程. ,
当父进程执行完fork以后它的时间片没有用完,那么通常更有可能是父进程先执行.,父进程的时间片用完了,那么子进程就会被调度再执行.
结论 : 不确定
3.父进程能否多次调用fork?
可以的, 每一次调用都会创建一个新的子进程 (父进程没有限制)
注意: 创建完子进程后是需要回收的, 不然会产生非常多的僵尸进程.
4.子进程能不能调用fork ?
没有问题,那么子进程同样可以调用fork. 如果子进程调用fork的话,相当于子进程又创建了子进程.称为孙进程.
(TCP并发服务器模型)
参数如果传 exit(0)表示正常退出 exit(1)表示异常退出.这个1是返回给操作系统的不过在DOS好像不需要这个返回值 一般0为正常推出,其它数字为异常,其对应的错误可以自己指定。
printf是往标准输出流写的,标准输出流是一个行缓冲. 而这里没有换行符 ,这里实际上只是写到标准输出流的缓冲区里,并没有写到我们的终端上. 所以在终端上是看不到的,但是接下来我们直接执行了exit()这个函数结束当前进程的时候回去刷新流,所以这个在标准输出流的字符串还是会被输出到终端上.
而exit下面的这条就不会输出在终端上.
调试linux下的socket程序时,发现服务器端收到的信息只有在客户端结束后才会显示收到的信息,但是如果在printf中加入换行符,就会立刻输出。原因是因为Unix系统一般有行缓存。而’\n’可视为行刷新标志。
只要把printf(“1”);改成printf(”1\n”);
下面情况下会刷新缓存:
1 强制刷新标准输出缓存fflush(stdout);
2 放到缓冲区的内容中包含/n;
3 缓冲区已满;
4 需要从缓冲区拿东西到时候,如执行scanf;
如何来创建一个进程呢?
我们在shell下去执行一个程序,shell创建一个子进程来执行我们的程序,同样我们可以在程序中再指定让我们当前的进程去执行另外一个程序.
如何在进程中去执行另外一个程序 ?
当我们用exec去指定执行另外一个程序的时候,那么这时候我们当前进程的内容就会被指定程序替换,包括当前进程中的代码段,数据段,堆栈等等.都会被新的程序所替换,原先的进程,除了进程号不变其他的内容都被替换掉了
父进程创建子进程的时候,子进程继承了父进程的内容包括代码和数据,也就是说子进程它实际上跟父进程执行的是同一个程序.那么有些时候我们希望让父子进程都 能够执行不同的程序,这时候就可以利用exec函数族来实现.
第一步:
父进程创建子进程,这个时候子进程会去复制父进程中的内容. 那么子进程复制完之后呢 ? 子进程开始执行
第二步
让子进程调用exec函数族, 让子进程去执行我们指定的一个程序,这时候父进程是不受任何影响的
什么时候会用到?最典型的例子就是我们的shell
shell它是一个程序, shell程序也叫命令行解释器,可以根据命令行上 用户的输入去执行一个指定的程序,实际上shell基本的工作原理就是我们这里提到的.shell相当于是一个父进程那么我们用户在shell下输入一个程序的名称,那么shell进程接收到我们用户输入的程序名以后,shell创建了一个子进程, 那么这个子进程默认是复制了父进程里的内容. 也就是说子进程它也是个shell. 但是我们希望执行的是用户指定的程序, 所以shell创建的子进程里面会调用exec去执行用户指定的这个程序,所以我们就能看到在shell下 ,我们的指定程序被执行了.所以说我们学习完这个exec函数以后,可以自己实现一个最简单的shell
execl 函数 可以用来执行指定的程序,
第一个参数 :字符串类型, 指定要执行程序的名称(包含完整路径)可以是绝对路径,也可以是相对路径.
第二个参数 : 可变的 . 字符串类型 都是传递给要指定程序的参数,第1个参数传的是path对应的程序的名称, 后面依次传的内容是,传递给这个程序中的选项和参数的内容 是个可变参数,相当于我们的main函数中的char *argv[]参数,
第三个参数 : 必须是NULL, 是个空指针, 不然函数执行失败.
执行成功了,我们当前程序的内容被新的程序所替换. 这些内容也就相当于不存在了, 是没有任何方回值的,因为程序内容已经不存在了,
如果失败了返回-1 (指定程序不存在, 或者参数错误, 当前用户没有权限去执行这个程序)
要做错误提示处理
execlp 函数 可以用来执行指定的程序,
第一个参数 :file类型, 直接指定我们要执行的程序的名称 , 而不需要包含路径, 那不包含路径,我们系统 怎么去找这个程序呢? 通过当前起名shell的环境变量PATH (这里面放的是我们shell的搜索路径) 这个环境变量当我们城西执行的时候,会被继承过来. 会直接在PATH所包含的路径中去找这个程序,找到执行,找不到报错, 文件不存在.
第二个参数 : 可变的 . 字符串类型 都是传递给要指定程序的参数,第1个参数传的是path对应的程序的名称, 后面依次传的内容是,传递给这个程序中的选项和参数的内容 是个可变参数,相当于我们的main函数中的char *argv[]参数,
第三个参数 : 必须是NULL, 是个空指针, 不然函数执行失败.
execl 和 execlp两个的区别就是第一个参数
execv 和 execl 就差一个字母 v代表的是数组
execv
第一个参数 : 要执行程序的路径,
第二个参数 : 字符指针数组 , 把要传递的参数放到这个字符数组里. ,然后把这个数组作为参数传 进来 .
这样的好处是 ,数组中的内容是可以改的, 也就是说我们通过execv这种方式的话,我们同一个函数,可以调不同的程序, 就是你数组中放的是什么程序,什么选项,什么参数, execv就执行对于的程序带上对应的选项和参数,比第一种execl更加灵活
system :想要去执行另外一个程序最简单的方法就是通过这个函数
在当前进程中,自动创建一个子进程, 去执行command 里面包含的程序,命令(父进程会一直等待这个命令执行完了,父进程才会继续往下执行,如果不想自己创建子进程, 再用execl去执行另外一个程序,那么最简单的方法就是直接调用system去执行另外一个程序,执行完之后,返回当前程序,继续往下执行)执行成功了返回这个命令的返回值, 失败时返回EOF()
第一个参数 : 字符串, 要执行的程序. 里面可以包含参数和选项.
会自动先创建一个子进程 , 子进程去执行这个command所包含的程序 , 然后我们的父进程会一直等待这个命令执行完了, 父进程才会继续往下执行.

进程回收
1.当一个父进程创建好一个子进程之后, 那么子进程, 在结束的时候必须由其父进程及时的回收, 包括子进程的一些返回值,以及子进程的结束的状态, 都可以由父进程通过回收的方式来获取.
2.那么如果父进程比子进程早结束了,那么这时候子进程就会变成"孤儿进程" ,而孤儿进程会自动的由系统中的init进程收养来负责回收 ,
3.那么如果一个子进程结束了,没有及时被父进程回收, 会出现僵尸进程. 也就是说子进程的其他资源 实际上都已经被释放了.但是它的PCB(进程控制块)依然存在.直到父进程也结束了, 子进程变成孤儿进程, 它的PCB才会被init进程所释放.
wait函数 : 功能, 回收一个子进程 .如果回收成功了返回的是子进程的进程号. 一定是一个大于0的数, 如果失败了,返回-1并且设置error,
如果当前的子进程没有结束 , 父进程一直阻塞. 直到子进程结束, 父进程回收成功.返回为止.所以调用wait的时候,父进程是有可能会阻塞的.
若有多个子进程. 可以回收当前子线程的任意一个子进程 , 不会指定是哪一个子进程,只要当前子进程又任意一个进程结束了,whit就能够回收成功. 创建子进程可能有先后顺序,那么同样子进程结束的时候, 它的顺序是任意的. 这一点并不影响. 父进程调用wait哪一个子进程结束了 ,就先回收 ,哪一个子进程.并且返回的是回收成功的子进程的进程号.
如果父进程创建了三个子进程, 父进程就需要调用 三次wait来分别回收这三个子进程, 那么回收的顺序你得看子进程结束的顺序. 哪一个子进程先结束就先回收哪一个子进程 .
第一个参数: 整型指针参数, 保存了子进程的返回值, 以及子进程的结束状态.(传参的时候注意: 定义一个整型变量,然后把这个整型变量的地址传进来, 而不应当定义一个整型指针.因为整型指针定义好以后是一个随机数,是个野指针)这样这个返回值和结束状态, 就保存到整型变量里了.还可以传空指针, 传空指针就意味着告诉系统子进程结束了,我父进程不打算接收它的返回值.以及它的结束方式, 可以把子进程直接释放 .
返回值 : pid_t 等价于整型,
感兴趣可以去头文件里看一下这几个宏定义, 实际上status里面, 已经定义好了, 哪些位是什么含义 .
第一个参数 : 指定回收的对象 指定回收哪一个子进程(wait不行)
第二个参数 : 跟wait是一样的.
第三参数 : 指定回收的方式
0表示阻塞的方式(如果当前子进程没有结束,那么父进程会一直阻塞,直到子进程结束,回收成功才返回(跟wait一样))
WNOHANG是非阻塞(如果子进程没有结束,父进程不会阻塞,而是立刻从waitpid返回,返回0(当前要回收的子进程没有结束,如果返回的是大于0表示子进程已经结束了,并且回收成功)
守护进程最大区别跟交互进程不一样 . 交互进程可以让用户通过终端 于程序进行交互有输入 有输出, 而守护进程运行的生命周期于我们的交互进程时间要长很多 , 一般来说当我们的linux系统运行起来之后,那么它在我们的后台 会启动很多的守护进程 , 而且守护进行会提供各种各样的服务, 那么直到我们的系统关闭的时候, 守护进程才会结束 ,
守护进程运行的生命周期 , 运行的时候会非常长 , 跟普通的交互进程不一样, 普通的交互进程运行完了进程也就结束了, linux使用非常广泛, 服务都是通过守护进程来提供的(各种网络服务 http服务…打印服务 )
守护进程特点 :
始终在后台运行 : 好处 不会影响到其他进程的运行 , 如果我进程一直在前台运行 , 前台运行的话意味着我们通过终端的输入, 都会被前台进程处理, 而守护进程在后台运行, 那么在前台就可以运行其他的进程 ,
前台进程可以通过终端输入 , 也向终端输出 , 而后台进程只能向终端输出, 不能从终端输入 ,
守护进程跟一般的后台进程还不一样 , 守护进程终端是无法使用的 .没有办法向终端输出. 守护进程跟终端无关 ,
交互进程创建后是和终端相关联的 , 而守护进程于终端无关
守护进程运行起来以后, 会周期性的去执行某些代码 . 比如说: 每隔一段时间去做一些系统维护工作, 或者说我们实现一个服务, 比如说: 我们检测到有网络服务器请求了 ,我们的后台守护进程会自动的去处理. ps查看所有的进程信息 , ps -ef 可以看一下基本上, 进程它的名称如果最后一个字母是D的话Daemon 就代表它是 一个守护进程 .
linux系统为了更好去管理相关的进程 ,(linux是一个多任务的操作系统 ,运行起来以后会同时跑很多的进程)为了更好的去管理这些进程 , linux里面有进程组合会话的划分 ,
什么是进程组 :
但我们运行起程序的时候 ,就创建了一个进程来执行这个程序, 这个进程创建的时候同时创建了一个新的进程组. 理解 :每一个运行的程序实际上就对应着一个进程组 . 那么我们在这个进程中,又创建了子进程, 那么子进程实际上和这个父进程一样也属于同一个进程组. 所以进程组的理解可以和程序
一一对应起来的.
什么是会话 :
一组相关的一个或多个相关的进程组的集合 就是会话
一般什么时候会创建一个会话呢 ?当我们用户打开一个终端 (比如,ubuntu登录好以后,我们运行起一个终端,那么终端一打开以后,这时候会默认运行一个shell进程)的时候系统就创建了一个会话 .那么在这个会话中运行的第一个进程shell进程, 称为会话的首进程 ,会话组的组长 . 那么我们在shell下创建的所有的执行的所有的程序 . 创建的所有的进程 都属于同一个会话 .这个终端也把它称之为这个会话的控制终端 .
一个会话只能打开一个控制终端 .(可以不打开)但是有控制终端的话,最多只能有一个
控制终端对会话有什么影响呢 ?
系统有规定, 当一个 控制终端关闭的时候, 所有依附于这个终端的会话中的所有的进程都会被结束
我们通常说的交互进程 ,它实际上就是有一个终端, 就是属于某一个终端 , 那当这个终端关闭的时候 , 无论这个进程是在前台运行还是在后台运行 , 进程都会结束 ,而守护进程很显示 ,我们希望守护进程只要系统不关闭它始终在运行 . 那么这样的话我们就必须 , 让守护进程和终端无关 , 这样的话当终端关闭的时候 , 守护进程才能不受影响 . (守护进程为什么要脱离终端, 就是为了让终端关闭时守护进程不被结束)
守护进程创建分为5个步骤
第一步 :
首先程序运行起来以后 ,(shell下运行一个程序的时候,这时候创建个进程,是交互进程 当前这个进程依附于所打开的终端的) 我们在程序中创建一个子进程 , 然后父进程退出 , 子进程继续往下执行, 父进程结束了, 那子进程就变成了孤儿进程, 它的父进程就变成了init进程 ,init进程收养 .
除了变成孤儿进程之外, 我们的子进程还会在后台运行 .我们原先执行程序的时候默认是在前台 ,当父进程结束的时候 , 子进程自动在后台运行 .子进程虽然在后台运行 ,但这个时候子进程依然依附于当前的终端 . 也就是说当前终端关闭了, 子进程还是会结束 . 所以这个时候我们的子进程还不是一个守护进程.
第二步 :
(创建守护进程最关键步骤)子进程中要创建一个新的会话 .为什么 要创建一个新的会话呢?因为原先的会话有一个终端, (原先的会话是我们打开终端的时候创建的)那么我们的进程创建的时候继承了过来,所以我们要在子进程中创建一个新的会话, 子进程成为新的会话组长, 子进程不再属于原先的会话.
第三步 :
修改进程的当前工作目录, chdir("/"); chdir("/tmp");会把当前进程的工作目录修改,你可以指向根目录, 也可以指向根目录下的tmp目录, 这两个有什么区别 ? 权限不一样 , 对于普通用户来说的话根目录只能读可执行但是不可写 . 而tmp目录 的权限是3个7, 意味着所有的用户在tmp下都是可读可写可执行的 . 为什么要更改当前目录呢? 守护进程一直在后台运行 , 几乎就不会结束 ,那么守护进程它的当前工作目录所在的文件系统是无法被卸载的 .那么守护进程创建的时候它的当前工作目录有可能指向任意一个目录,所以为了避免它所在的文件系统无法被卸载, 一般都会把服务进程的当前工作目录指向一个永远不需要被卸载的目录 , 比如说根目录, 或者根目录下的tmp
第四步 :
修改当前进程的文件权限掩码 (文件权限掩码 ? 当我们创建新的文件的时候 ,权限掩码就是我们指定的文件权限 会和这个掩码的反 进行一个与&操作得到我们最终的权限 , 而我们的守护进程里面也有可能创建一个新文件 ,而我们通常不希望对守护进程创建的文件的权限加以限制 , 所以我们一般都会调用函数umack()参数就是新的文件权限掩码, 一般设置成0 掩码是0的话就相当于它不会去屏蔽任何的权限位 .这样的话我们在守护进程中创建的文件 我们指定的是什么权限 它最终权限就是什么 , 不受掩码的影响)
设置的时候注意 : 我们这个掩码新的文件权限掩码 , 只对我们的当前进程有效, 不会影响到其他进程.
第五步 :
关闭从父进程继承的一些文件, 守护进程实际上是一个子进程 , 是父进程创建的 , 那么子进程在创建的时候会把父进程中打开的文件都继承过来 , 那么我们需要把这些文件都关闭掉 ,通过一个for循环来关闭打开的文件 . 从0开始,因为文件描述符最小就是0, 父进程到底打开了几个文件, 子进程也不确定 ,通过一个函数getdtablesize() 这个函数返回的是当前进程 能够打开的最大文件个数(系统设定) 虽然我不知道继承了几个文件 ,但是我知道文件描述符最小是0 最大是这个返回值 , 那么我用for循环把这个区间所有打开的文件都关闭 , 后面如果守护进程需要访问其他文件可以自己打开用open 等都可以,
为什么要关闭呢? 一个程序执行的时候,系统会自动打开标准输入 ,标准输出, 标准错误,这三个流, 而这三个流都指向终端 , 而我们的守护进程是和终端无关的 . 所以这三个流是无法使用的 .所以直接把它关掉. 你也可以从定向 把这三个流重定向指向某一个文件, 然后往文件中输出 , 或者成文件中输入 也可以, 一般都是把这三个都关掉 . 因为守护进程已经没有终端了 , 这三个流一般来说也不会去使用它 ,