1.进程组和会话
1.1 概念和特性
进程组,也称之为作业。BSD于1980年前后向Unix中增加的一个新特性。代表一个或多个进程的集合。每个进程都属于一个进程组。在waitpid函数和kill函数的参数中都曾使用到。操作系统设计的进程组的概念,是为了简化对多个进程的管理。
当父进程,创建子进程的时候,默认子进程与父进程属于同一进程组。进程组ID==第一个进程ID(组长进程)。所以,组长进程标识:其进程组ID==其进程ID
可以使用kill -SIGKILL -进程组ID(负的)来将整个进程组内的进程全部杀死。【kill_multprocess.c】
组长进程可以创建一个进程组,创建该进程组中的进程,然后终止。只要进程组中有一个进程存在,进程组就存在,与组长进程是否终止无关。
进程组生存期:进程组创建到最后一个进程离开(终止或转移到另一个进程组)。
一个进程可以为自己或子进程设置进程组ID
1.2 创建会话
创建一个会话需要注意以下6点注意事项:
1.调用进程不能是进程组组长,该进程变成新会话首进程(session header)
2.该进程成为一个新进程组的组长进程。
3.需有root权限 (ubuntu不需要)
4.新会话丢弃原有的控制终端,该会话没有控制终端
5.该调用进程是组长进程,则出错返回
6.建立新会话时,先调用fork, 父进程终止,子进程调用setsid
让子进程去调用创建会话是因为第一点中调用进程不能是进程组长,父进程本身就是进程组组长,不能调用setsid;就是让一个不是进程组长的进程去创建会话成为组长和会长,脱离原先的组
1.3 getsid函数
获取进程所属的会话ID
pid_t getsid(pid_t pid);
成功:返回调用进程的会话ID;失败:-1,设置errno
pid为0表示察看当前进程session ID
ps ajx命令查看系统中的进程。参数a表示不仅列当前用户的进程,也列出所有其他用户的进程,参数x表示不仅列有控制终端的进程,也列出所有无控制终端的进程,参数j表示列出与作业控制相关的信息。
组长进程不能成为新会话首进程,新会话首进程必定会成为组长进程。
1.4 setsid函数
创建一个会话,并以自己的ID设置进程组ID,同时也是新会话的ID。
pid_t setsid(void);
成功:返回调用进程的会话ID;失败:-1,设置errno
调用了setsid函数的进程,既是新的会长,也是新的组长。
练习:fork一个子进程,并使其创建一个新会话。查看进程组ID、会话ID前后变化【session.c】
2.守护进程
Daemon(精灵)进程,是Linux中的后台服务进程,通常独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。一般采用以d结尾的名字。
Linux后台的一些系统服务进程,没有控制终端,不能直接和用户交互。不受用户登录、注销的影响,一直在运行着,他们都是守护进程。如:预读入缓输出机制的实现;ftp服务器;nfs服务器等。
创建守护进程,最关键的一步是调用setsid函数创建一个新的Session,并成为Session Leader。
创建守护进程模型:
1.创建子进程,父进程退出
- 所有工作在子进程中进行形式上脱离了控制终端
2.在子进程中创建新会话
setsid()函数
使子进程完全独立出来,脱离控制
3.改变当前目录位置(可以更改到根目录)
chdir()函数
防止占用可卸载的文件系统
也可以换成其它路径
程序执行的时候可以通过chdir函数指定程序在哪个工作目录下工作,守护进程就像服务器一样一直运行着,为了防止宕机就需要将程序的工作目录更改到不可随意删除的目录下,防止程序文件在执行的过程中被卸载了
4.重设文件权限掩码
umask()函数
防止继承的文件创建屏蔽字拒绝某些权限
增加守护进程灵活性
5.关闭文件描述符
继承的打开文件不会用到,浪费系统资源,无法卸载
开始执行守护进程核心工作守护进程退出处理程序模型
注意:
- 第5点中,关闭程序的输入文件描述符0(没关闭前是作用在终端上的),打开文件null,它将优先选取文件描述符表中最小的文件描述符,也就是0,也就是说fd(null文件)继承了终端的输入,同时将输出和标准错误重定向到了fd。
- 这时程序就不再控制终端了(0,1,2都跑去null文件了),加上6的while一直循环,程序不会宕机,实现了守护进程(独立终端并执行着特殊事情的进程)
3.线程概念
3.1 什么是线程
LWP:light weight process 轻量级的进程,本质仍是进程(在Linux环境下),线程号,标识线程身份交给CPU,CPU按照LWP去划分时间轮片,分配程序执行的时间
进程:独立地址空间,拥有PCB
线程:有独立的PCB,但没有独立的地址空间(共享)
区别:在于是否共享地址空间。独居(进程);合租(线程)。
Linux下:线程:最小的执行单位
进程:最小分配资源单位,可看成是只有一个线程的进程。
进程号LWP是顺着进程id16343继续往后编的,因为线程实际上就是进程,火狐浏览器启动的时候,会在线程池内启动大量的线程,而CPU会将这些线程(都处在线程16343内)看作进程,为此进程16343获取CPU的资源速度就更快,16343的速度就更快
3.2 Linux内核线程实现原理
类Unix系统中,早期是没有“线程”概念的,80年代才引入,借助进程机制实现出了线程的概念。因此在这类系统中,进程和线程关系密切。
1. 轻量级进程(light-weight process),也有PCB,创建线程使用的底层函数和进程一样,都是clone
2. 从内核里看进程和线程是一样的,都有各自不同的PCB,但是PCB中指向内存资源的三级页表是相同的
3. 进程可以蜕变成线程
4. 线程可看做寄存器和栈的集合
5. 在linux下,线程最是小的执行单位;进程是最小的分配资源单位
察看LWP号:ps –Lf pid 查看指定线程的lwp号。
三级映射:进程PCB --> 页目录(可看成数组,首地址位于PCB中) --> 页表 --> 物理页面 --> 内存单元
参考:《Linux内核源代码情景分析》 ----毛德操
对于进程来说,相同的地址(同一个虚拟地址)在不同的进程中,反复使用而不冲突。原因是他们虽虚拟址一样,但,页目录、页表、物理页面各不相同。相同的虚拟址,映射到不同的物理页面内存单元,最终访问不同的物理页面。
但!线程不同!两个线程具有各自独立的PCB,但共享同一个页目录,也就共享同一个页表和物理页面。所以两个PCB共享一个地址空间。
实际上,无论是创建进程的fork,还是创建线程的pthread_create,底层实现都是调用同一个内核函数clone。
如果复制对方的地址空间,那么就产出一个“进程”;如果共享对方的地址空间,就产生一个“线程”。
因此:Linux内核是不区分进程和线程的。只在用户层面上进行区分。所以,线程所有操作函数 pthread_* 是库函数,而非系统调用。
3.3 线程共享资源
1.文件描述符表
2.每种信号的处理方式
- 信号哪个线程抢到就哪个线程去调用内核去处理,但线程之间mask不共享,不推荐线程和信号混着用
3.当前工作目录
4.用户ID和组ID
5.内存地址空间(.text/.data/.bss/heap/共享库) -->全局变量
3.4 线程非共享资源
1.线程id
2.处理器现场和栈指针(内核栈)
3.独立的栈空间(用户空间栈)
4.errno变量
- 本质是全局变量,在bata里,但是它不共享
5.信号屏蔽字
6.调度优先级
3.5 线程优、缺点
优点:
- 提高程序并发性
- 开销小
- 数据通信、共享数据方便
缺点:
- 库函数,不稳定
- 调试、编写困难、gdb不支持
- 对信号支持不好
优点相对突出,缺点均不是硬伤。Linux下由于实现方法导致进程、线程差别不是很大。