Linux系统编程之守护进程、线程
1.守护进程
守护进程(Daemon Process),也就是通常说的Daemon(精灵)进程,是 Linux 中的后台服务进程,通常独立于控制终端并且周期性地执行某种任务或等待处理 某些发生的事件。一般采用以 d 结尾的名字。
Linux 后台的一些系统服务进程,没有控制终端,不能直接和用户交互。不受用户登录、注销的影响,一直在运行着,他们都是守护进程。如:预读入缓输出机制的实现;ftp 服务器;nfs 服务器等。
创建守护进程,最关键的一步是调用 setsid
函数创建一个新的 Session,并成为 Session Leader。
1.1 进程组
多个进程的集合就是进程组,也称之为作业。BSD 于 1980 年前后向 Unix 中增加的一个新特性。代表一个或多个进程的集合。每个进程都属于一个进程组。在waitpid
函数和 kill
函数的参数中都曾使用到。操作系统设计的进程组的概念,是为了简化对多个进程的管理。
当父进程,创建子进程的时候,默认子进程与父进程属于同一进程组。进程组 ID == 第一个进程 ID(组长进程)。 所以,组长进程标识:其进程组 ID == 其进程 ID
可以使用 kill -SIGKILL -进程组 ID(负的)
来将整个进程组内的进程全部杀死。
组长进程可以创建一个进程组,创建该进程组中的进程,然后终止。只要进程组中有一个进程存在,进程组就存在,与组长进程是否终止无关。
进程组生存期:进程组创建到最后一个进程离开(终止或转移到另一个进程组)。
一个进程可以为自己或子进程设置进程组 ID
下面介绍几个常用的进程组函数:
得到当前进程所在的进程组的组 ID
pid_t getpgrp(void);
获取指定的进程所在的进程组的组 ID,参数 pid 就是指定的进程
pid_t getpgid(pid_t pid);
将某个进程移动到其他进程组中或者创建新的进程组
int setpgid(pid_t pid, pid_t pgid);
函数参数:
pid:
某个进程的进程 ID
pgid:
某个进程组的组 ID
如果 pgid 对应的进程组存在,pid 对应的进程会移动到这个组中,pid != pgid
如果 pgid 对应的进程组不存在,会创建一个新的进程组,因此要求 pid == pgid, 当前进程就是组长了
函数返回值:
函数调用成功返回 0,失败返回 - 1
1.2 会话
会话 (session) 是由一个或多个进程组组成的,一个会话可以对应一个控制终端,也可以没有。一个普通的进程可以调用 setsid()
函数使自己成为新 session 的领头进程(会长),并且这个 session 领头进程还会被放入到一个新的进程组中。先来看一下 setsid() 函数的原型:
1.3 setsid()函数
创建一个会话,并以自己的 ID 设置进程组 ID,同时也是新会话的 ID。
函数原型:
1 |
|
函数返回值:
- 成功:返回调用进程的会话 ID
- 失败:-1,设置 errno
1.4 getsid()函数
获取进程所属的会话 ID
函数原型
1 |
|
函数返回值:
- 成功:返回调用进程的会话 ID
- 失败:-1,设置 errno
==pid 为 0 表示察看当前进程 session ID==
使用这两个函数的注意事项:
1.调用这个函数的进程不能是组长进程,如果是,该函数调用失败,如何保证这个函数能调用成功呢?
- 先 fork () 创建子进程,终止父进程,让子进程调用这个函数
2.如果调用这个函数的进程不是进程组长,会话创建成功
- 这个进程会变成当前会话中的第一个进程,同时也会变成新的进程组的组长
- 该函数调用成功之后,当前进程就脱离了控制终端,因此不会阻塞终端
1.5 创建守护进程
- 创建子进程,父进程退出
- 在子进程中创建新会话
setsid()函数
使子进程完全独立出来,脱离控制 - 改变当前目录位置
chdir()函数
防止占用可卸载的文件系统, 也可以换成其它路径 - 重设文件权限掩码
umask()
函数 防止继承的文件创建屏蔽字拒绝某些权限,增加守护进程灵活性 - 通常根据需要,关闭/重定向 文件描述符
- 开始执行守护进程业务逻辑,通常是while()循环
小案例
写一个守护进程,每隔 2s 获取一次系统时间,并将得到的时间写入到磁盘文件中。
1 |
|
2.线程
2.1 什么是线程?
线程是轻量级的进程(LWP:light weight process),在 Linux 环境下线程的本质仍是进程。在计算机上运行的程序是一组指令及指令参数的组合,指令按照既定的逻辑控制计算机运行。操作系统会以进程为单位,分配系统资源,可以这样理解,==进程是资源分配的最小单位,线程是操作系统调度执行的最小单位==。
进程:独立地址空间,拥有 PCB
线程:有独立的 PCB,但没有独立的地址空间(共享)
区别:在于是否共享地址空间。 独居(进程);合租(线程)。
2.2 Linux 内核线程实现原理(了解即可)
类 Unix 系统中,早期是没有“线程”概念的,80 年代才引入,借助进程机制实现出了线程的概念。因此在这类系统中,进程和线程关系密切。
- 轻量级进程(light-weight process),也有 PCB,创建线程使用的底层函数和进程一样,都是 clone
- 从内核里看进程和线程是一样的,都有各自不同的 PCB,但是 PCB 中指向内存资源的三级页表是相同的
- 进程可以蜕变成线程
- 线程可看做寄存器和栈的集合
- 在 linux 下,线程最是小的执行单位;进程是最小的分配资源单位
查看 LWP 号:ps –Lf pid
查看指定线程的 lwp 号(线程号)。
三级映射:进程 PCB –> 页目录(可看成数组,首地址位于 PCB 中) –> 页表 –> 物理页面 –> 内存单元
对于进程来说,相同的地址(同一个虚拟地址)在不同的进程中,反复使用而不冲突。原因是他们虽虚拟地址一样,但,页目录、页表、物理页面各不相同。相同的虚拟地址,映射到不同的物理页面内存单元,最终访问不同的物理页面。
但!线程不同!两个线程具有各自独立的 PCB,但共享同一个页目录,也就共享同一个页表和物理页面。所以两个 PCB 共享一个地址空间。 实际上,无论是创建进程的 fork,还是创建线程的 pthread_create,底层实现都是调用同一个内核函数 clone。 如果复制对方的地址空间,那么就产出一个“进程”;如果共享对方的地址空间,就产生一个“线程”。 因此:==Linux 内核是不区分进程和线程的==。只在用户层面上进行区分。所以,线程所有操作函数 pthread_* 是库函数,而非系统调用。
2.4 线程共享资源和非共享资源
共享资源
1.文件描述符表
2.每种信号的处理方式
3.当前工作目录
4.用户 ID 和组 ID
5.内存地址空间 (.text/.data/.bss/heap/共享库)
非共享资源
1.线程 id
2.处理器现场和栈指针(内核栈)
3.独立的栈空间(用户空间栈)
4.errno 变量
5.信号屏蔽字
6.调度优先级
线程优缺点
优点: 1. 提高程序并发性 2. 开销小 3. 数据通信、共享数据方便
缺点: 1. 库函数,不稳定 2. 调试、编写困难、gdb 不支持 3. 对信号支持不好
优点相对突出,缺点均不是硬伤。Linux 下由于实现方法导致进程、线程差别不是很大。
3.创建线程
3.1 线程函数
每一个线程都有一个唯一的线程 ID,ID 类型为 pthread_t
,这个 ID 是一个无符号长整形数(%lu),如果想要得到当前线程的线程 ID,可以调用如下函数:
pthread_t pthread_self(void); // 返回当前线程的线程ID
在一个进程中调用线程创建函数,就可得到一个子线程,和进程不同,需要给每一个创建出的线程指定一个处理函数,否则这个线程无法工作。
1 |
|
参数:
- thread: 传出参数,是无符号长整形数,线程创建成功,会将线程 ID 写入到这个指针指向的内存中
- attr: 线程的属性,一般情况下使用默认属性即可,写 NULL
- start_routine: 函数指针,创建出的子线程的处理动作,也就是该函数在子线程中执行。
- arg: 作为实参传递到 start_routine 指针指向的函数内部
返回值:线程创建成功返回 0,创建失败直接返回对应的错误号,不会设置errono,所以用
perror()
是行不通的,这点要注意,应直接用strerror()
小案例
循环创建多个子线程
1 |
|
编译运行,结果如下:
可以看到,不符合我们的预期想法,错误原因在于,子线程将主线程中i的地址传递了进去,因此子线程运行时会去读取主线程里的i值,而主线程里的i是动态变化的,不固定。所以,应该传递值而不是地址。
修改的代码如下所示:
1 |
|
编译运行,结果如下:
注意
编译时,要指定参数-lpthread
指定动态库libpthread.so
3.2 线程间全局变量共享
直接看个代码,在子线程里更改全局变量,看主线程里的该变量有啥变化:
编译运行,结果如下
可以看到,子线程里更改全局变量后,主线程里也跟着发生变化。
3.线程退出
在编写多线程程序的时候,如果想要让线程退出,但是不会导致虚拟地址空间的释放(针对于主线程),我们就可以调用线程库中的线程退出函数,只要调用该函数当前线程就马上退出了,并且不会影响到其他线程的正常运行,不管是在子线程或者主线程中都可以使用。
1 |
|
- 参数:线程退出的时候携带的数据,当前子线程的主线程会得到该数据。如果不需要使用,指定为 NULL
注意区别三者
- exit(); 退出当前进程。
- return: 返回到调用者那里去。
- pthread_exit(): 退出当前线程。
下面是线程退出的示例代码,可以在任意线程的需要的位置调用该函数:
1 |
|
编译运行,结果如下:
4.线程回收
4.1线程回收函数
线程和进程一样,子线程退出的时候其内核资源主要由主线程回收,线程库中提供的线程回收函叫做 pthread_join()
,这个函数是一个阻塞函数,如果还有子线程在运行,调用该函数就会阻塞,子线程退出函数解除阻塞进行资源的回收,函数被调用一次,只能回收一个子线程,如果有多个子线程则需要循环进行回收。
另外通过线程回收函数还可以获取到子线程退出时传递出来的数据,函数原型如下:
1 |
|
参数:
- thread: 要被回收的子线程的线程 ID
- retval: 二级指针,指向一级指针的地址,是一个传出参数,这个地址中存储了 pthread_exit () 传递出的数据,如果不需要这个参数,可以指定为 NULL
返回值:线程回收成功返回 0,回收失败返回错误号。
4.2 回收子线程数据
在子线程退出的时候可以使用 pthread_exit() 的参数将数据传出,在回收这个子线程的时候可以通过 phread_join() 的第二个参数来接收子线程传递出的数据。接收数据有很多种处理方式,下面来举个例子:
1 |
|
编译运行,结果如下:
5.线程分离
在某些情况下,程序中的主线程有属于自己的业务处理流程,如果让主线程负责子线程的资源回收,调用 pthread_join()
只要子线程不退出主线程就会一直被阻塞,主要线程的任务也就不能被执行了。
在线程库函数中为我们提供了线程分离函数 pthread_detach()
,调用这个函数之后指定的子线程就可以和主线程分离,当子线程退出的时候,其占用的内核资源就被系统的其他进程接管并回收了
。线程分离之后在主线程中使用 pthread_join()
就回收不到子线程资源了。
1 |
|
小案例
1 |
|
6.线程取消
函数原型
1 | //杀死一个线程。需要到达取消点(保存点),即进行一次系统调用 |
- 参数:
thread:
待杀死的线程id
- 返回值:
- 成功:0
- 失败:直接返回错误号errno
如果,子线程没有到达取消点(即系统调用), 那么 pthread_cancel
无效。 我们可以在程序中,手动添加一个取消点。使用 pthread_testcancel()
; 成功被 pthread_cancel()
杀死的线程,返回 -1.使用pthead_join
回收。
小案例
主线程调用pthread_cancel杀死子线程
1 |
|
编译运行,结果如下
终止线程方式:
总结:终止某个线程而不终止整个进程,有三种方法:
从线程主函数
return
。这种方法对主控线程不适用,从main 函数 return 相当于调用 exit。一个线程可以调用
pthread_cancel
终止同一进程中的另一个线程。线程可以调用
pthread_exit
终止自己。
7.控制原语对比
8.线程属性
8.1 基本概念
linux 下线程的属性是可以根据实际项目需要,进行设置。之前我们讨论的线程都是采用线程的默认属性,默认属性已经可以解决绝大多数开发时遇到的问题。如我们对程序的性能提出更高的要求那么需要设置线程属性,比如可以通过设置线程栈的大小来降低内存的使用,增加最大线程个数。
1 | typedef struct |
主要结构体成员:
线程分离状态
线程栈大小(默认平均分配)
线程栈警戒缓冲区大小(位于栈末尾)
属性值不能直接设置,须使用相关函数进行操作,初始化的函数为 pthread_attr_init
,这个函数必须在 pthread_create
函数之前调用。之后须用 pthread_attr_destroy
函数来释放资源。
线程属性主要包括如下属性:作用域(scope)、栈尺寸(stack size)、栈地址(stack address)、优先级(priority)、 分离的状态(detached state)、调度策略和参数(scheduling policy and parameters)。默认的属性为非绑定、非分离、缺省的堆栈、与父进程同样级别的优先级。
8.1线程属性设置分离线程
1 .pthread_attr_t attr
创建一个线程属性结构体变量
2 .pthread_attr_init(&attr);
初始化线程属性
3 .pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
设置线程属性为 分离态
4 .pthread_create(&tid, &attr, tfn, NULL);
借助修改后的 设置线程属性 创建为分离态的新线程
5 .pthread_attr_destroy(&attr);
销毁线程属性
小案例
调整线程状态,使线程创建出来就是分离态,代码如下:
1 |
|
编译运行,如下所示:
如图,pthread_join
报错,说明线程已经自动回收,设置分离成功。
9.线程使用注意事项
主线程退出其他线程不退出,主线程应调用
pthread_exit
避免僵尸线程
pthread_join
pthread_detach
pthread_create
指定分离属性被 join 线程可能在 join 函数返回前就释放完自己的所有内存资源,所以不应当返回被回收线程栈中的值;
malloc
和mmap
申请的内存可以被其他线程释放应避免在多线程模型中调用
fork
除非,马上exec
,子进程中只有调用fork
的线程存在,其他线程在子进程中均pthread_exit
信号的复杂语义很难和多线程共存,应避免在多线程引入信号机制