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 信号量
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 的使用;