进程组和会话
进程组是多个进程的集合,而会话是多个进程组的集合
每个进程都属于一个进程组,当父进程创建子进程的时候,默认子进程与父进程属于同一进程组
进程组ID = 第一个进程ID(组长进程)
可以使用kill -SIGKILL-进程组ID
(负的)来将整个进程组内的进程全部杀死
只要进程组中有一个进程存在,进程组就存在,与组长进程是否终止无关
创建会话
创建一个会话需要注意以下6点注意事项:
- 创建会话的调用进程不能是进程组组长,该进程变成新会话首进程(session header)
- 该进程成为一个新进程组的组长进程
- 需要有root权限(ubuntu不需要)
- 新会话丢弃原有的控制终端,该会话没有控制终端
- 若该调用进程是组长进程,则出错返回
- 建立新会话时,先调用fork,父进程终止(因为fork后父进程就成为了组长,而组长是不能创建会话的),子进程调用setside()
当一个进程创建一个新线程后,进程会坍缩为一个线程,通常我们称其为主线程,称新创建的那个线程为子线程
getsid函数
pid_t getsid(pid_t pid);
获取进程所属的会话ID
setsid函数
pid_t setsid(void);
创建一个会话,并以自己的ID设置进程组ID,同时也是新会话的ID
即调用了setsid
函数的进程,即使新的组长,也是新的会长(PID、进程组ID、会话ID 三ID合一)
ps:执行ps ajx
命令可显示所有进程的PPID、PID、PGID和SID
守护进程
即Daemon(恶魔)进程,是Linux中的后台服务进程,通常独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件,命名一般以d结尾
Linux后台的一些系统服务进程没有控制终端,不能直接和用户交互,也不受用户登录、注销的影响,一直在运行着,它们都是守护进程,比如:预读入缓输出机制的实现、ftp服务器、nfs服务器等
创建守护进程最关键的一步是调用setsid
函数创建一个新的Session,并成为Session Leader
创建守护进程步骤
- fork子进程,让父进程终止(因为父进程是组长,不能让他去创建会话)
- 子进程调用
setsid()
函数创建新会话 - 通常根据需要,通过
chdir()
函数改变工作目录位置,防止目录被卸载 - 通常根据需要,重设umask文件权限掩码,以免影响新文件的创建权限 022 — 755 0345 — 432 r—wx-w- 422
- 通常根据需要,关闭文件描述符(或者重定向到/dev/null)
- 守护进程具体的业务逻辑 while()
※编程练习:创建守护进程
待
线程概念
在Linux中,线程(LWP,Light Weight Process)是轻量级的进程,本质上仍是进程
- 进程:有独立的pcb,有独立的地址空间,是分配资源的最小单位
- 线程:有独立的pcb,没有独立的地址空间,只能共享同一进程的内存空间,是CPU执行的最小单位
ps:虽然线程有自己独立的pcb,但是pcb中指向内存资源的三级页表跟进程是相同的,因此线程没有自己的内存空间,只能共享同一进程的内存空间
查看某进程下的线程号:ps -Lf 进程id
(注意查看的是线程号,不是线程ID)
线程共享
线程共享资源:
- 文件描述符表
- 每种信号的处理方式
- 当前工作目录
- 用户ID和组ID
- 内存地址空间(.text/.data/.bss/heap/共享库)
线程非共享资源:
- 线程id
- cpu现场和栈指针(内核栈)
- 独立的栈空间(用户空间栈)
- errno变量
- 信号屏蔽字
- 调度优先级
总结:独享栈空间(内核栈、用户栈),共享.text/.data/.bss/heap/共享库,共享全局变量(errno除外)
线程控制原语
当代码中使用了pthread
,就需要在makefile的目标规则中,添加 -lpthread
,确保在编译和链接过程中正确地链接到pthread
库
pthread_self函数
pthread_t pthread_self(void);
获取线程ID,其作用对应进程中的getpid()
函数
返回值:本线程ID
ps:线程ID数据类型是无符号整型unsigned long,输出格式说明符为%lu
线程中检查出错返回
fprintf(stderr, "xxx error: %s\n", strerror(ret));
if(ret != 0) {
fprintf(stderr, "xxx error: %s\n", strerror(ret));
exit(1);
}
pthread_create函数
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void (start_routine) (void *), void *arg);
创建一个新线程,其作用对应进程中的fork()
函数
参数:
- 参数1:传出参数,新创建的线程ID
- 参数2:线程属性(见最后一节设置分离属性),可以传NULL表示使用默认属性
- 参数3:线程回调函数,创建成功后,
ptherad_create
函数返回时,该函数会被自动调用 - 参数4:参3的参数,如果回调函数没有参数,传NULL
返回值:
- 成功:0
- 失败:error
编程练习:循环创建多个子线程
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <dirent.h>
#include <errno.h>
#include <signal.h>
void sys_err(const char *str) {
perror(str);
exit(1);
}
void *tfn(void *arg) {
int i = (int)arg;
sleep(i);
printf("---i am %dth thread: pid = %d, tid = %lu\n", i + 1, getpid(), pthread_self());
return NULL;
}
int main(int argc, char *argv[]) {
int i, ret;
pthread_t tid;
for (i = 0; i < 5; i++) {
ret = pthread_create(&tid, NULL, tfn, (void *)i); //传参采用值传递,借助强制类型转换
if (ret != 0) {
sys_err("pthread create error");
}
}
sleep(i); //让主线程睡眠,防止直接终止,为了让子线程有时间去执行回调函数
printf("main : i am man: pid = %d, tid = %lu\n", getpid(), pthread_self());
return 0;
}
pthread_exit函数
void pthread_exit(void *retval);
退出当前线程
参数:退出值,无退出值时,传NULL
ps:exit(0)
表示退出进程,return
表示返回到调用者那里去
pthread_join函数
阻塞等待线程退出,获取线程退出状态,其作用对应进程中的waitpid()
函数
int pthread_join(pthread_t thread, void **retval);
参数:
- thread:线程ID
- **retval:传出参数,被回收线程的退出值,若线程异常结束(比如detach后再join),值为-1(注意此参数是**类型,比如传的是指针,还要对指针再取地址)
ps:在进程中,是父进程回收子进程,但在线程中没有这个概念,因为线程之间地位都是平等的
pthread_detach函数
实现线程分离,让线程在结束后自动释放其资源,而不需要调用pthread_join()
函数来回收这些资源
int pthread_detach(pthread_t thread);
pthread_cancel函数
int pthread_cancel(pthread_t thread); 杀死线程,其作用对应进程中的kill()
函数
返回值:
- 成功:0
- 失败:error
如果子线程没有到达取消点(保存点), 那么pthread_cancel
无效
我们可以在程序中手动添加一个取消点:pthread_testcancel()
原语对比
线程控制原语 👉 线程控制原语
pthread_create() 👉 fork();
pthread_self() 👉 getpid();
pthread_exit() 👉 exit(); / return
pthread_join() 👉 wait() / waitpid()
pthread_cancel() 👉 kill()
pthread_detach()
线程属性
掌握创建线程时就让其带上分离属性,就不用之后再调用detach()
函数了:
pthread_attr_t attr;
创建一个线程属性结构体变量pthread_attr_init(&attr);
初始化线程属性pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
设置线程属性为分离态pthread_create(&tid, &attr, tfn, NULL);
借助修改后的设置线程属性,创建为分离态的新线程pthread_attr_destroy(&attr);
销毁线程属性所占用的资源
线程使用注意事项
- 主线程退出其他线程不退出,主线程应调用
pthread_exit
- 如何避免僵尸进程:
- pthread_join()回收
- pthread_detach()分离
- pthread_create()时指定分离属性
malloc
和mmap
申请的内存可以被其他线程释放- 应避免在多线程模型中调用
fork
,除非马上exec,新创建的子进程中只有调用fork的线程存在,其他线程在子进程中均pthread_exit
- 信号的复杂语义很难和多线程共存,应避免在多线程引入信号机制