1. 多进程编程
1.1 fork
#include <sys/types.h>
#include <unistd.h>
// 调用失败返回 -1 设置 errno
pid_t fork( void );
子进程返回 0,父进程返回子进程 PID;
信号位图被清除(父进程的信号处理函数不再对新进程起作用)
1.2 exec 系列系统调用
替换当前进程映像,一般情况下不会返回,出错后返回 -1 并设置 errno;
注意:exec 函数不会关闭原程序打开的文件描述符,除非该 fd 设置了 SOCK_CLOEXEC 属性;
1.3 处理僵尸进程
#include <sys/types.h>
#include <sys/wait.h>
// 调用失败返回 -1 设置 errno
pid_t wait( int* stat_loc );
pid_t waitpid( pid_t pid, int* stat_loc. int options );
waitpid
只等待 pid 指定的子进程;pid 取值为 -1,和 wait
相同,等待任意一个子进程结束;
options 取值是 WNOHANG 时,waitpid
是非阻塞的:pid 指定的子进程没有结束或意外终止,立即返回 0;子进程正常退出,返回子进程 PID;
1.4 IPC 信号量
1.4.1 semget
创建一个新的信号量集,或者获取一个已经存在的信号量集;
#include <sys/sem.h>
// 成功返回一个正整数,标识信号量集
// 失败返回 -1 设置 errno
int semget( key_t key, int num_sems. int sem_flags );
key 用来标识一个全局唯一的信号量集;传递特殊键值 IPC_PRIVATE(值为 0)可以保证总是创建新的信号量集;
num_sems 指定信号量集中信号量的数目;(若是创建,必须指定;若是获取,设为 0)
sem_flags 指定该信号量的权限, 后 9 位与open
中的 mode 一致;
#include <sys/sem.h>
struct ipc_perm
{
key_t key; /* 键值 */
uid_t uid; /* 所有者的有效用户 ID */
gid_t gid; /* 所有者的有效组 ID */
uid_t cuid; /* 创建者的有效用户 ID */
gid_t cgid; /* 创建者的有效组 ID */
mode_t mode; /* 访问权限 */
/* 省略其他填充字段 */
};
struct semid_ds
{
struct ipc_perm sem_perm; /* 信号量的操作权限 */
unsigned long int sem_nsems; /* 信号量集的信号量数目 */
time_t sem_otime; /* 最后一次调用 semop 的时间 */
time_t sem_ctime; /* 最后一次调用 semctl 的时间 */
/* 键值 */
};
semid_ds 是与信号量集相关联的内核数据结构,semget
作用是创建并初始化它,会将 sem_otime 设为 0,sem_ctime 设为当前系统时间;
1.4.2 semop
改变信号量的值,是原子操作;
unsigned short semval; /* 信号量的值 */
unsigned short semzcnt; /* 等待信号值变为 0 的进程数量 */
unsigned short semncnt; /* 等待信号值增加的进程数量 */
pid_t sempid; /* 最后一次执行 semop 操作的进程 ID */
int semop( int sem_id, struct sembuf* sem_ops, size_t num_sem_ops );
sem_id 是 semget
返回的信号量集标识符;sem_ops 指向 sembuf 数组;
struct sembuf
{
unsigned short int sem_num;
short int sem_op;
short int sem_flg;
}
sem_num 为信号集中信号量的编号;
SEM_UNDO 的含义是,当进程退出时取消正在进行的 semop
操作,会更新进程的 semadj 变量,,来跟踪进程对信号量的修改情况;
1.4.3 semctl
对信号量直接控制
int semctl( int sem_id, int sem_num, int command, ... );
command 为要执行的命令,有些命令需要第 4 个参数,推荐如下形式
union semun
{
int val; /*用于 SETVAL 命令*/
struct semid_ds* buf; /*用于 IPC_STAT 和 IPC_SET 命令*/
unsigned short* array; /*用于 GETALL 和 SETALL 命令*/
struct seminfo* __buf; /*用于 IPC_INFO 命令*/
};
常用命令为
命令 | 含义 | 成功时返回值 |
---|---|---|
IPC_RMID | 移除信号量集,唤醒所有等待该信号量集的进程(semop 返回错误,errno 为 EIDRM) | 0 |
SETVAL | 将信号量的值设为 semun.val ,同时内核数据中的 semid_ds.sem_ctime 被更新 | 0 |
1.5 共享内存
最高效的 IPC 机制;
1.5.1 shmget
创建一段新的共享内存或获取一段已经存在的共享内存;
#include <sys/shm.h>
// 成功返回正整数,为共享内存的标识;失败返回 -1 设置 errno
int shmget( key_t key, size_t size, int shmflg );
这三个参数与 semget
含义相同,size 指定共享内存的大小,单位是字节;
shmflg 支持额外的两个标志:
- SHM_HUGETLB;系统使用“大页面”分配共享内存,类似于
mmap
的 MAP_HUGETLB 标志; - SHM_NORESERVE;不为共享内存保留交换分区,类似于
mmap
的 MAP_NORESERVE 标志;这样当物理内存不足时,对该共享内存执行写操作将触发 SIGSEGV 信号;
shmget
创建共享内存,所有字节都被初始化为 0,与之关联的内核数据结构为 shmid_ds 被创建和初始化;
struct shmid_ds
{
struct ipc_perm shm_perm; /* 共享内存的操作权限 */
size_t shm_segsz; /* 共享内存的大小,单位是字节 */
__time_t shm_atime; /* 对这段内存最后一次调用 shmat 的时间 */
__time_t shm_dtime; /* 对这段内存最后一次调用 shmdt 的时间 */
__time_t shm_ctime; /* 对这段内存最后一次调用 shmctl 的时间 */
__pid_t shm_cpid; /* 创建者的 PID */
__pid_t shm_lpid; /* 最后一次执行 shmat 或 shmdt 操作的进程 PID */
shmatt_t shm_nattach; /* 关联到此共享内存的进程数量 */
/* 省略填充字段 */
};
初始化时,shm_lpid、shm_nattach、shm_atime、shm_dtime 设置为 0,shm_ctime 设置为当前时间;
1.5.2 shmat 和 shmdt
关联/分离到进程的地址空间
void* shmat( int shm_id, const void* shm_addr, int shmflg );
int shmdt( const void* shm_addr );
shm_addr 指定将共享内存关联到进程的哪块空间地址;
- shm_addr 为 NULL,由操作系统选择,推荐的做法;
- shm_addr 非空,并且 SHM_RND 未设置,共享内存关联到 addr 指定地址处;
- shm_addr 非空,并且设置了 SHM_RND ,被关联的地址是 [shm_addr - (shm_addr%SHMLBA)];
shmflg 还支持如下标志:
- SHM_RDONLY;进程仅能读取共享内存中的内容;
- SHM_REMAP;如果地址 shm_addr 已经被关联到一段共享内存上,重新关联;
- SHM_EXEC;指定对共享内存段的执行权限;
shmat 成功返回被关联的地址,失败返回(void *) -1 并设置 errno;将 shm_nattach++,shm_lpid 设置为调用进程 PID,shm_atime 设置为当前时间;
shmdt 成功时返回 0,失败返回 -1 设置 errno;将 shm_nattach–,shm_lpid 设置为调用进程 PID,shm_dtime 设置为当前时间;
1.5.3 shmctl
// 失败时返回 -1 设置 errno
int shmctl( int shm_id, int command, struct shmid_ds* buf );
命令 | 含义 | 成功时返回值 |
---|---|---|
SETVAL | 将信号量的值设为 semun.val ,同时内核数据中的 semid_ds.sem_ctime 被更新 | 0 |
1.5.4 共享内存的 POSIX 方法
通过打开同一个文件, mmap
可以实现进程间的内存共享;
Linux 提高了另一种利用 mmap
在无关进程间共享内存的方式,无需任何文件的支持;
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
// 创建或打开一个 POSIX 共享内存对象
int shm_open( const char* name, int oflag, mode_t mode );
int shm_unlink( const char* name );
shm_open
成功返回一个文件描述符,该 fd 可用于后续的 mmap
调用,失败返回 -1 设置 errno。
shm_unlink
将 name 指定共享内存对象标记为等待删除;当所有使用该共享对象的进程都使用 ummap
将它从进程中分离后,系统将销毁该对象;
ftruncate(fd, length)
会将参数 fd 指定的文件大小改为参数 length 指定的大小。
1.6 消息队列
两个进程间传递二进制块数据一种简单有效的方式;
每个数据块都有一个特定的类型,接收方可以根据类型来选择地接收数据;
1.6.1 msgget
创建或获取消息队列
#include <sys/msg.h>
int msgget( key_t key, int msgflg );
参数含义与 semget
一致,与之关联的内核数据结构 msqid_ds 被创建并初始化;
struct msqid_ds
{
struct ipc_perm msg_perm; /* 操作权限 */
time_t msg_stime; /* 最后一次调用 msgsnd 的时间 */
time_t msg_stime; /* 最后一次调用 msgrcv 的时间 */
time_t msg_stime; /* 最后一次被修改的时间 */
unsigned long __msg_cbytes; /* 消息队列中已有的字节数 */
msgqnum_t msg_qnum; /* 消息队列中已有的消息数 */
msglen_t msg_qbytes; /* 消息队列允许的最大字节数 */
pid_t msg_lspid; /* 最后执行 msgsnd 的进程 PID */
pid_t msg_lrpid; /* 最后执行 msgrcv 的进程 PID */
};
1.6.2 msgsnd
把一条消息添加到消息队列中;
// 成功返回 0,失败返回 -1
int msgsnd( int msqid, const void* msg_ptr, size_t msg_sz, int msgflg );
msg_ptr 指向一个准备发送的消息
struct msgbuf
{
long mtype; /* 消息类型,必须为正整数 */
char mtext[512]; /* 消息数据 */
};
msg_sz 是消息数据部分的长度,可以为 0 表示没有消息数据;
msgflg 设置 IPC_NOWAIT 标志时,表示非阻塞;如果消息队列满,默认情况下会阻塞,如果设置了 IPC_NOWAIT 将立即返回并设置 errno 为 EAGAIN;
处于阻塞状态的 msgsnd
可能被中断:
- 消息队列被移除,会立即返回,errno 为 EIDRM;
- 程序接收到信号,立即返回,errno 为 EINTR;
1.6.3 msgrcv
从消息队列中获取消息
// 成功返回 0,失败返回 -1
int msgrcv( int msqid, void* msg_ptr, size_t msg_sz, long int msgtype, int msgflg );
(1)msg_ptr 用于存储接收到的消息;
(2)msgtype 指定接收何种类型的消息;
- 为 0,表示读取消息队列中的第一个消息;
- 大于 0,读取第一个类型为 msgtype 的消息;
- 小于0,读取第一个类型值比 msgtype 绝对值小的消息;
(3)msgflg
- IPC_NOWAIT;如果消息队列为空,立即返回,errno 设为 ENOMSG;
- MSG_EXCEPT;msgtype 大于 0,接收第一个非 msgtype 的消息;
- MSG_NOERROR;如果消息数据部分长度超过了 msg_sz,将其截断;
(4)阻塞的 msgrcv
被中断情况与 msgsnd
一致;
1.6.4 msgctl
// 失败返回 -1
int msgctl( int msqid, int command, struct msqid_ds* buf );
1.7 IPC 命令
上述 3 种进程间通信方式都使用了全局唯一的键值来描述一个共享资源;
Linux 下 ipcs 命令可以查看当前系统上的共享资源实例;
1.8 进程间传递文件描述符
可以利用 UNIX 域 socket 在进程间传递特殊辅助数据
利用 sendmsg
和 recvmsg
可以发送附属数据
2. 多线程编程
内核线程:运行在内核空间,由内核调度;
用户线程:运行在用户空间,由线程库调度;
当进程的一个内核线程获得 CPU 使用权时,加载并允许一个用户线程,内核线程相当于用户线程运行的“容器”;
一个进程可以拥有 M 个内核线程和 N 个用户线程,M <= N,它们之间的比值是固定的;
(1)完全在用户空间实现;
M:N = 1:N
线程库利用 longjmp 来切换线程;一个进程的所有执行线程共享该进程的时间片,对外表现出相同的优先级;
优点:创建和调度线程的速度非常快,不需要内核参与,不占用额外的内核资源;
缺点:一个进程的多个线程无法运行在不同的 CPU 上;
(2)完全由内核调度;
M:N = 1:1,与上述优缺点互换
(3)双层调度;结合了前两种方式的优点;
2.1 线程库
Linux 下目前基本使用的都是 NPTL,采用 1:1 方式实现的;
2.2 创建和结束线程
(1)
#include <pthread.h>
// 成功返回 0,失败返回错误码
int pthread_create( pthread_t* thread, const pthread_attr_t* attr,
void* (*start_routine)( void* ), void* arg );
thread 的类型 pthread_t 是无符号长整形,为新线程的标识符;
attr 设置新线程的属性,设为 NULL 代表使用默认线程属性;
一个用户可打开的线程数量不能超过 RLIMIT_NPROC 限制;
(2)在结束时最好调用如下函数;
void pthread_exit( void* retval );
retval 向线程的回收者传递其退出信息;
(3)等待其他线程结束(回收)
int pthread_join( pthread_t thread, void** retval );
会一直阻塞直到被回收的线程结束;
成功返回 0,失败返回错误码;
错误码 | 描述 |
---|---|
EDEADLK | 可能引起死锁;比如两个线程互相针对对方调用 pthread_join 或 线程对自身调用 pthread_join |
EINVAL | 目标线程不可回收,或已经有其他线程在回收该目标线程 |
ESRCH | 目标线程不存在 |
(4)异常终止一个线程;
int pthread_cancel( pthread_t thread );
接收到这个请求的目标线程可以决定是否允许被取消以及如何取消;
int pthread_setcancelstate( int state, int* oldstate );
int pthread_setcanceltype( int type, int* oldtype );
第一个参数分别用于设置线程的取消状态(是否允许取消)和取消类型(如何取消)
第二个参数记录了原来的状态和类型;
state 可选值为:
- PTHREAD_CANCEL_ENABLE,允许线程被取消,线程创建时的默认状态
- PTHREAD_CANCEL_DISABLE,禁止线程被取消;如果收到取消请求,会将请求挂起,直到该线程允许被取消
type 可选值为:
- PTHREAD_CANCEL_ASYNCHRONOUS,线程随时都可以被取消,接收到取消请求立即采取行动;
- PTHREAD_CANCEL_DEFERRED,允许目标线程推迟行动,直到调用了下面几个取消点函数中的一个:pthread_join、pthread_testcancel、pthread_cond_wait、pthread_cond_timedwait、sem_wait 和 sigwait;
2.3 线程属性
各种线程属性全部包含在一个字符数组中;
#include <bits/pthreadtypes.h>
#define __SIZEOF_PTHREAD_ATTR_T 36
typedef union
{
char __size[__SIZEOF_PTHREAD_ATTR_T];
long int __align;
} pthread_attr_t;
// 初始化线程属性对象
int pthread_attr_init( pthread_attr_t* attr );
// 销毁线程属性对象,被销毁的对象只有再次初始化之后才能继续使用
int pthread_attr_destroy( pthread_attr_t* attr );
// 获取或设置线程属性对象的某个属性
int pthread_attr_getdetachstate( const pthread_attr_t* attr, int* detachstate );
int pthread_attr_setdetachstate( pthread_attr_t* attr, int detachstate );
int pthread_attr_getstackaddr( const pthread_attr_t* attr, void** stackaddr );
int pthread_attr_setstackaddr( pthread_attr_t* attr, void* stackaddr );
int pthread_attr_getstacksize( const pthread_attr_t* attr, size_t* stacksize );
int pthread_attr_setstacksize( pthread_attr_t* attr, size_t stacksize );
int pthread_attr_getstack( const pthread_attr_t* attr, void** stackaddr, size_t* stacksize );
int pthread_attr_setstack( pthread_attr_t* attr, void* stackaddr, size_t stacksize );
int pthread_attr_getguardsize( const pthread_attr_t* attr, size_t* guardsize);
int pthread_attr_setguardsize( pthread_attr_t* attr, size_t guardsize);
int pthread_attr_getschedparam( const pthread_attr_t* attr, struct sched_param* param );
int pthread_attr_setschedparam( pthread_attr_t* attr, const struct sched_param* param );
int pthread_attr_getschedpolicy( const pthread_attr_t* attr, int* policy );
int pthread_attr_setschedpolicy( pthread_attr_t* attr, int policy );
int pthread_attr_getinheritsched( const pthread_attr_t* attr, int* inherit);
int pthread_attr_setinheritsched( pthread_attr_t* attr, int inherit );
int pthread_attr_getscope( const pthread_attr_t* attr, int* scope );
int pthread_attr_setscope( pthread_attr_t* attr, int scope );
- detachstate,线程的脱离状态;
- PTHREAD_CREATE_JOINABLE;可被回收;(默认值)
- PTHREAD_CREATE_DETACH;脱离线程,退出时自行释放资源;
- stackaddr 和 stacksize,线程堆栈的起始地址和大小;(一般由系统自动管理)
- guardsize,保护区域大小;如果大于 0,系统创建线程时会在其堆栈的尾部额外分配 guardsize 字节空间,作为保护堆栈不被错误地覆盖的区域;如果通过
pthread_attr_setstackaddr
或pthread_attr_setstack
手动设置线程的堆栈,会忽略该属性; - schedparam,线程调度参数;该结构体目前只有一个整性类型成员 sched_priority,表示线程运行优先级;
- schedpolicy,线程调度策略;
- SCHED_FIFO;先进先出调度
- SCHED_RR;轮转算法调度,这两种调度算法都具备实时调度功能,只能用于以超级用户身份运行的进程;
- SCHED_OTHER; (默认值)
- inheritsched,是否继承调用线程的调度属性;PTHREAD_INHERIT_SCHED 和 PTHREAD_EXPLICIT_SCHED,前者表示沿用其创建者的线程调度参数,这种情况下再设置新线程的调度参数将没有任何效果;后者表明需要为新线程设定新的调度参数;
- scope,线程间竞争 CPU 的范围,即线程优先级的有效范围;目前 Linux 只支持 PTHREAD_SCOPE_SYSTEM 表明与系统中所有线程一起竞争 CPU 的使用;
2.4 POSIX 信号量
接下来是专门用于线程同步的机制;
#include <semaphore.h>
// 成功时返回 0,失败返回 -1 设置 errno
int sem_init( sem_t* sem, int pshared, unsigned int value );
int sem_destroy( sem_t* sem );
int sem_wait( sem_t* sem );
int sem_trywait( sem_t* sem );
int sem_post( sem_t* sem );
(1)sem_init
初始化一个未命名信号量;pshared 指定信号量的类型,其值为 0,表示这个信号量是当前进程局部信号量,否则可以在多个进程间共享;value 指定信号量的初始值;
注意:初始化一个已经被初始化的信号量将导致不可预期的后果;
(2)sem_destroy
销毁信号量,释放其占用的内核资源;如果销毁一个正被其他线程等待的信号量,将导致不可预期的结果;
(3)sem_wait
以原子操作方式将信号量的值减 1,如果信号量值为 0,将被阻塞;
(4)sem_trywait
和 sem_wait
类似,不过其始终立即返回;当信号量值为 0 时,其返回 -1 设置 errno 为 EAGAIN;
(5)sem_post
以原子操作方式将信号量加 1;
2.5 互斥锁
2.5.1 基础 API
#include <pthread.h>
// 成功时返回 0,失败返回错误码
int pthread_mutex_init( pthread_mutex_t* mutex, const pthread_mutexattr_t* mutexattr );
int pthread_mutex_destroy( pthread_mutex_t* mutex );
int pthread_mutex_lock( pthread_mutex_t* mutex );
int pthread_mutex_trylock( pthread_mutex_t* mutex );
int pthread_mutex_unlock( pthread_mutex_t* mutex );
(1)初始化互斥锁,mutexattr 指定互斥锁的属性,设为 NULL,表示使用默认属性;
可以使用如下方式初始化一个互斥锁,实际上是将互斥锁各字段都初始化为 0;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
(2)pthread_mutex_trylock
对已经加锁的互斥锁操作时,返回错误码 EBUSY;
2.5.2 互斥锁属性
// 成功时返回 0,失败返回错误码
int pthread_mutexattr_init( pthread_mutexattr_t* attr );
int pthread_mutexattr_destroy( pthread_mutexattr_t* attr );
int pthread_mutexattr_getpshared( const pthread_mutexattr_t* attr, int* pshared );
int pthread_mutexattr_setpshared( pthread_mutexattr_t* attr, int pshared );
int pthread_mutexattr_gettype( const pthread_mutexattr_t* attr, int* type );
int pthread_mutexattr_settype( pthread_mutexattr_t* attr, int type );
- pshared 的可选值为:
- PTHREAD_PROCESS_SHARED,互斥锁跨进程共享;
- PTHREAD_PROCESS_PRIVATE,只能被锁初始化线程隶属同一个进程的线程共享;
- type 指定互斥锁的类型
- PTHREAD_MUTEX_NORMAL,普通锁(默认类型);请求该锁的线程形成一个等待队列,在该锁解锁后按优先级获得它;同一个线程重复加锁会发生死锁;
- PTHREAD_MUTEX_ERRORCHECK,检错锁;一个线程对一个已经加锁的检错锁再次加锁时,返回 EDEADLK;对一个已经被其他线程加锁的检错锁解锁,或对一个已经解锁的检错锁再次解锁,返回 EPERM;
- PTHREAD_MUTEX_RECURSIVE,嵌套锁;允许一个线程在释放锁之前多次对它加锁而不发生死锁,释放锁时必须执行相应次数的解锁操作;对一个已经被其他线程加锁的嵌套锁解锁,或对一个已经解锁的嵌套锁再次解锁,返回 EPERM;
- PTHREAD_MUTEX_DEFAULT,默认锁;
2.6 条件变量
条件变量提供了一种线程间的通知机制:当某个共享数据达到某个值时,唤醒等待这个共享数据的线程;
#include <pthread.h>
// 成功时返回 0,失败返回错误码
int pthread_cond_init( pthread_cond_t* cond, const pthread_mutexattr_t* cond_attr );
int pthread_cond_destroy( pthread_cond_t* cond);
int pthread_cond_broadcast( pthread_cond_t* cond);
int pthread_cond_signal( pthread_cond_t* cond);
int pthread_cond_wait( pthread_cond_t* cond, pthread_mutex_t* mutex );
(1)初始化也可以采用如下方式
pthread_mutex_t mutex = PTHREAD_COND_INITIALIZER;
(2)销毁一个正在被等待的条件变量将失败并返回 EBUSY;
(3)pthread_cond_broadcast
以广播方式唤醒所有等待目标条件变量的线程;
pthread_cond_signal
唤醒一个等待目标条件的线程;具体哪个线程被唤醒,取决于线程的优先级和调度策略;
若想唤醒指定线程,需要使用 pthread_cond_broadcast
,在线程内判断是否是目标线程,若是开始执行,否则继续等待;
(4)pthread_cond_wait
等待目标条件变量,在调用pthread_cond_wait
前,必须确保 mutex 已经加锁;其在执行时,首先把调用线程放入条件变量的等待队列中,然后将 mutex 解锁;
2.7 多线程环境
可重入函数(线程安全的):一个函数可以被多个线程同时调用且不发生竞态条件;
2.7.1 进程和线程
多线程程序下的某个线程调用 fork
函数,新创建的子进程只有一个执行线程,是调用 fork
函数的那个线程的完整复制;并且子进程会自动继承 fork
时父进程中互斥锁(条件变量)的状态;
这可能会导致问题,因为子进程可能并不清除父进程中互斥锁的具体状态;
// 成功返回 0,失败返回错误码
int pthread_atfork( void (*prepare)(void), void (*parent)(void), void (*child)(void) );
该函数将建立 3 个 fork
句柄帮助我们清理互斥锁的状态,prepare 句柄将在 fork
调用创建出子进程前执行,可以用来锁住所有父进程中的互斥锁;parent 句柄则是在 fork
创建出子进程之后, fork
返回前,在父进程中被执行,释放所有在 prepare 句柄中被锁住的互斥锁;child 是 fork
返回之前,在子进程中被执行,释放所有在 prepare 句柄中被锁住的互斥锁;
在 prepare 中加锁时,会等待其他线程释放锁,因此可能会阻塞;
2.7.2 线程和信号
每个线程都可以独立的设置信号掩码
#include <pthread.h>
#include <signal.h>
// 成功返回 0,失败返回错误码
int pthread_sigmask( int how, const sigset_t* newmask, sigset_t* oldmask );
进程中的所有线程共享该进程的信号,线程库将根据线程掩码决定把信号发给哪个线程;所有线程共享信号处理函数;应该定义一个专门线程来处理所有的信号
#include <signal.h>
// 成功返回 0,失败返回错误码
int sigwait( const sigset_t* set, int* sig );
将一个信号发送给指定的线程:
#include <signal.h>
// 成功返回 0,失败返回错误码
int pthread_kill( pthread_t thread, int sig );
如果 sig 为 0,不发送信号,可以利用这种方式检测目标线程是否存在;
3 进程池和线程池
(1)如何选择子进程:
- 主进程使用某种算法来主动选择子进程;如随机算法或 Round Robin 算法;
- 主进程和所有子进程通过一个共享的工作队列来同步,子进程都睡眠在该工作队列上;当有新的任务到来时,主进程将任务添加到工作队列中;
(2)通知子进程,并传递必要的数据;使用预先建好的一条管道;
多线程中可以把数据定义为全局的,就会被所有线程共享;
3.1 半同步/半异步进程池
(1)主函数创建监听 socket;
(2)主函数使用 监听 socket 初始化进程池类(进程池类使用单例模式,由静态成员函数和静态变量实现,在函数内部 new 该类,返回这个指针,之后由用户使用完毕后,用户负责释放这个指针)
进程池类构造时,创建多个子进程;(这些子进程最终都会执行主函数剩余部分,因此把关闭 连接socket 和 delete 进程池类 都放在主函数后面执行,对外表现是:在哪里初始化就在哪里销毁,逻辑清晰)
(3)运行
每个进程都创建自己的 epollfd
父进程只负责监听 socket 和 各种信号,如果有请求连接,就将通过管道发送给某个子进程;
子进程负责 与父进程通信管道 、连接 socket 和 各种信号,子进程还负责读取连接 socket 上的数据,具体业务逻辑会 fork
子进程负责;
3.2 半同步/半反应堆线程池
请求队列由 list 实现,并使用互斥锁进行保护;
在 C++ 中使用 pthread_create
时,该函数的第 3 个参数必须指向一个静态函数;在静态函数中使用类的成员变量,需要把类的对象作为参数传递给该静态函数;
为每个客户都维护了一个 http_conn 类
主线程负责监听 socket 和 连接 socket,对连接 socket 的读写也都与主线程完成,具体业务逻辑由子线程完成;