目录
1.5 信号相关函数(kill、raise、alarm、pause、abort)
2.5 openmp 线程标准(相对于 posix 线程标准)
一、信号
1.1 信号的概念
信号是软件层面的中断。信号的响应依赖于中断。信号分为标准信号和实时信号。
kill -l 可以查看系统中的信号:
core 文件是程序出错的现场,可以使用 gdb 对 core 文件进行调试。
1.2 signal()
signal(2) 可以为特定的信号 signum 注册一个新的处理函数 handler,并且返回之前的处理函数。当出现特定的信号 signum 就会调用 handler。假如 handler 为 SIGIGN,则信号会被忽视;若 handler 为 SIGDFL,则会执行默认的处理函数。
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
该函数实际的样子:
例子:
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
void sig_handler(int signum)
{
write(1, "1", 1);
}
int main()
{
// signal(SIGINT, SIGIGN);
signal(SIGINT, sig_handler);
for (int i = 0; i < 10; i++) {
write(1, "*", 1);
sleep(1);
}
exit(0);
}
ctrl + c 可以发出 SIGINT 信号,所以每次使用 ctrl + c 都会调用一次信号处理函数 sig_handler:
重点:信号会打断阻塞的系统调用!!
如果 ctrl + c 按的很快的话可以看到 sleep 系统调用会被打断。 比如 open 和 read 系统调用中的两个错误码:
所以在前面例子中系统调用失败可能是由于信号导致的假错误,这时候我们可以重新进行一次系统调用。
不能随意地在信号处理函数中往外跳。
1.3 可重入函数
信号的不可靠,比如说第一次调用还没结束,第二次调用就开始了(连续两个相同信号到来)。可以使用可重入函数解决,可重入函数在第一次调用还没结束时发生第二次调用不会出错。
所有的系统调用都是可重入的,部分库函数是可重入的。
memcpy() 的两个内存地址空间不能重叠,而 memmove() 可以。
1.4 信号的响应过程(重点)
信号从收到到响应有一个不可避免的延迟。在从 kernel 返回到 user 态的时候才会查看 mask 和 pending 位图的按位与,然后响应信号。
如何忽略掉一个信号的?(mask 清 0)
标准信号为什么要丢失(多次置 pending 为 1,只响应一次)。
在收到多个标准信号时,标准信号的响应没有严格的顺序。
在响应信号的时候,mask 置 0,防止重入。
1.5 信号相关函数(kill、raise、alarm、pause、abort)
1. kill(2) 系统调用可以发送任意信号给任意进程或进程组。
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
pid 有如下四种情况:
- 当 pid 为正数时,sig 信号被发送给 pid 指定的进程。
- 当 pid 为 0 时,sig 信号会被发送给调用进程的进程组内的所有进程。
- 当 pid 为 -1 时,sig 信号会被发送给当前进程有权限发送信号的每一个进程,除了 init 进程(1 号进程)。
- 当 pid 小于 -1 时,sig 信号会被发送给 pgid 为 -pid 的进程组内的所有进程。
sig 参数为 0 时,不发送任何信号,可以用于检测进程和进程组是否存在(错误码为 ESRCH)。
2. raise(3) 可以给当前进程或线程发送信号。
#include <signal.h>
int raise(int sig);
在单线程的程序中 raise() 等效于:
kill(getpid(), sig);
在多线程的程序中 raise() 等效于:
pthread_kill(pthread_self(), sig);
3. alarm(2) 系统调用可以定时发送一个 SIGALRM 信号(注意不要在一个程序中多次使用 alarm ,多次使用时,只有最后一个 alarm 生效)。
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
当 seconds 为 0 时,所有等待的 alarm 都被取消。
alarm 可以用于实现流量控制,有如下两种方式:
- 漏桶,就算海量的数据到来,还是以固定的速率处理数据,但没有数据的时候会死等。
- 令牌桶, 没有数据的时候会攒令牌,当数据到来的时候可以根据令牌数量处理更多的数据。
例子,mytbf,可以使用令牌桶来读取文件内容:
/* main.c */
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <signal.h>
#include <string.h>
#include "mytbf.h"
#define CPS 10
#define BUFSIZE 1024
#define BURST 100
int main(int argc, char **argv)
{
int sfd, dfd = 1;
char buf[BUFSIZE];
int len, ret, pos, token_nums;
mytbf_t *tbf;
if (argc < 2) {
fprintf(stderr, "Usage...\n");
exit(1);
}
tbf = mytbf_init(CPS, BURST);
if (tbf == NULL) {
fprintf(stderr, "tbf is NULL\n");
exit(1);
}
do {
if ((sfd = open(argv[1], O_RDONLY)) < 0) {
if (errno != EINTR) {
perror("open()");
exit(1);
}
}
} while (sfd < 0);
while (1) {
token_nums = mytbf_fetchtoken(tbf, BUFSIZE);
while ((len = read(sfd, buf, token_nums)) < 0) {
if (errno == EINTR)
continue;
perror("read()");
break;
}
if (len == 0)
break;
// return token which did not used
if (token_nums - len > 0) {
mytbf_returntoken(tbf, token_nums - len);
}
pos = 0;
while (len > 0) {
ret = write(dfd, buf + pos, len);
if (ret < 0) {
if (errno == EINTR)
continue;
perror("write()");
exit(1);
}
pos += ret;
len -= ret;
}
}
close(sfd);
mytbf_destroy(tbf);
exit(0);
}
/* mytbf.h */
#ifndef MYTBF_H__
#define MYTBF_H__
#define MYTBF_MAX 1024
typedef void mytbf_t;
mytbf_t *mytbf_init(int cps, int burst);
int mytbf_fetchtoken(mytbf_t *, int );
int mytbf_returntoken(mytbf_t *, int );
int mytbf_destroy(mytbf_t *);
#endif
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include "mytbf.h"
typedef void (*sighandler_t)(int);
static struct mytbf_st *job[MYTBF_MAX];
static int inited = 0;
static sighandler_t alrm_handler_save;
struct mytbf_st
{
int cps; // number of characters per second
int burst; // max number of token
int token; // number of token to send characters
int pos;
};
static int get_free_pos(void)
{
for (int i = 0; i < MYTBF_MAX; i++) {
if (job[i] == NULL) {
return i;
}
}
return -1;
}
static void alrm_handler(int s)
{
alarm(1);
for (int i = 0; i < MYTBF_MAX; i++) {
if(job[i] != NULL)
{
job[i]->token += job[i]->cps;
if(job[i]->token > job[i]->burst) {
job[i]->token = job[i]->burst;
}
}
}
}
static void module_unload(void)
{
signal(SIGALRM, alrm_handler_save);
// close the registered alarm
alarm(0);
for (int i = 0; i < MYTBF_MAX; i++) {
free(job[i]);
}
}
static void module_load(void)
{
alrm_handler_save = signal(SIGALRM, alrm_handler);
alarm(1);
// register a function to be called at normal process termination
atexit(module_unload);
}
mytbf_t *mytbf_init(int cps, int burst)
{
int pos;
pos = get_free_pos();
if (pos < 0) {
return NULL;
}
if (!inited) {
module_load();
inited = 1;
}
struct mytbf_st *me = (struct mytbf_st *)malloc(sizeof(struct mytbf_st));
if (me == NULL) {
return NULL;
}
me->token = 0;
me->cps = cps;
me->burst = burst;
me->pos = pos;
job[pos] = me;
return me;
}
static int min(int a, int b)
{
return (a > b) ? b : a;
}
int mytbf_fetchtoken(mytbf_t *tbf, int nums)
{
int n;
struct mytbf_st *me = (struct mytbf_st *)tbf;
if (nums <= 0) {
return -1;
}
while (me->token <= 0) {
// wait for signal
pause();
}
n = min(me->token, nums);
me->token -= n;
return n;
}
int mytbf_returntoken(mytbf_t *tbf, int nums)
{
struct mytbf_st *me = (struct mytbf_st *)tbf;
if (nums <= 0) {
return -1;
}
me->token += nums;
if (me->token > me->burst) {
me->token = me->burst;
}
return nums;
}
int mytbf_destroy(mytbf_t *tbf)
{
struct mytbf_st *me = (struct mytbf_st *)tbf;
job[me->pos] = NULL;
free(tbf);
return 0;
}
sig_atomic_t 修饰符可以保证被修饰的变量操作一定是原子的。
4. pause(2) 系统调用使调用进程睡眠,等待信号到来唤醒进程。
#include <unistd.h>
int pause(void);
5. abort(3) 会给当前进程发送一个 SIGABRT 信号,会结束当前进程并且产生一个 coredump 文件。
#include <stdlib.h>
void abort(void);
扩展,sigsuspend(),sigaction(),setitimer()
1.6 信号集
信号集类型:sigset_t。
相关函数:
- sigemptyset();
- sigfillset();
- sigaddset();
- sigdelset();
信号屏蔽字相关函数:
- sigprocmask(2),可以将信号设置为阻塞状态,即不予响应,并且可以将之前的信号集 sigset_t 返回。
#include <signal.h>
/* Prototype for the glibc wrapper function */
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
- sigsuspend(2) 系统调用可以原子地设置 mask,然后 pause() 等待信号的到来,最后 恢复原来的 mask。
#include <signal.h>
int sigsuspend(const sigset_t *mask);
sigaction(2) 系统调用功能和 signal() 类似,可以设置或改变信号的处理函数,但是该系统调用可以在设置信号屏蔽字,以在信号处理函数执行的时候屏蔽其他需要屏蔽的信号。
#include <signal.h>
int sigaction(int signum, const struct sigaction *act,
struct sigaction *oldact);
实时信号不会丢失,发送几次响应几次。
二、线程
2.1 线程的概念
线程有许多不同的标准,比如 posix 线程是一套标准,而不是实现。
线程标识:pthread_t。
进程相当于容器,用来承载线程(相同的进程号 pid,但轻量级进程号 lwp 不一样,并且会占用进程号):
pthread_equal(3) 能够比较线程的 id,pthread_self(3) 可以返回当前线程 id。
线程相关的函数在编译连接的时候大多要加上 -pthread(CFLAGS 和 LDFLAGS)。
2.2 线程的创建、终止,栈的清理
1. pthread_create(3) 可以创建一个新线程,执行成功时返回 0。
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
start_routine 指向线程执行的函数,arg 为该线程函数传入的唯一参数;attr 参数可以指定线程的一些属性;当线程创建成功时,其线程 id 会被放到 thread 中返回。
一个线程只有在如下几种情况发生时才会终止:
- pthread_exit(3);
- 从 start_routine() 返回,返回值就是线程的退出码;
- 线程可以被同一进程中的其他线程取消,pthread_cancel(3);
- 有线程调用了 exit(3),或者主线程从 main() 返回,此时所有线程都会终止;
线程的调度取决于调度器策略。
2. 线程的收尸,pthread_join(3),相当于进程的 wait()。此函数会等待 thread 指定线程的终止,如果线程已经终止则立即返回。
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
3. 栈的清理,pthread_cleanup_push() 和 pthread_cleanup_pop(),相当于钩子函数,当线程被取消时会自动调用,要成对使用。
#include <pthread.h>
void pthread_cleanup_push(void (*routine)(void *),
void *arg);
void pthread_cleanup_pop(int execute);
4. 线程的取消,pthread_cancel(3),可以取消线程使其终止,然后再为其收尸。
#include <pthread.h>
int pthread_cancel(pthread_t thread);
取消有 2 种状态:允许和不允许;
不允许又分为:异步 cancel,推迟 cancel(默认)-> 推迟至 cancel 点响应;
cancel 点:POSIX 定义的 cancel 点,都是可能引发阻塞的系统调用。
5. 线程分离,pthread_detach(3),不需要为分离出去的线程收尸。
例子,primes_thread.c,利用线程来计算质数:
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#define MAX 100
#define THREADNUM 100
void *thread_func(void *arg)
{
int i = *((int *)arg);
int flag = 1;
for (int j = 2; j <= i / 2; j++) {
if (i % j == 0) {
flag = 0;
break;
}
}
if (flag) {
printf("%d\n", i);
}
pthread_exit(arg);
}
int main()
{
pthread_t thread_ids[THREADNUM] = {0};
int err;
void *retvalue;
for (int i = 2; i < MAX; i++) {
int *ip = (int *)malloc(sizeof(int));
*ip = i;
err = pthread_create(thread_ids + i, NULL, thread_func, ip);
if (err) {
fprintf(stderr, "pthread_create");
exit(1);
}
}
for (int i = 0; i < THREADNUM; i++) {
if (thread_ids[i] != 0) {
pthread_join(thread_ids[i], &retvalue);
free(retvalue);
}
}
exit(0);
}
2.3 线程同步(互斥量、条件变量、信号量、读写锁)
线程竞争例子,thread_contention.c,使用 10 个线程对 indexs 全局变量进行自增 10000000 次:
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#define THREADNUM 10
int indexs = 0;
void *thread_func(void *arg)
{
for (int i = 0; i < 10000000; i++) {
indexs++;
}
pthread_exit(NULL);
}
int main()
{
pthread_t thread_ids[THREADNUM] = {0};
int err;
for (int i = 0; i < THREADNUM; i++) {
err = pthread_create(thread_ids + i, NULL, thread_func, NULL);
if (err) {
fprintf(stderr, "pthread_create");
exit(1);
}
}
for (int i = 0; i < THREADNUM; i++) {
if (thread_ids[i] != 0) {
pthread_join(thread_ids[i], NULL);
}
}
printf("%d\n", indexs);
exit(0);
}
理想中的结果应该是 10 x 10000000,但是运行程序后结果不是这个,并且每次运行都不一样,这是因为 indexs++ 并非原子的,而想让程序避免冲突就需要用到线程同步。
线程同步的一种方法是使用互斥量(pthread_mutex_t)。
互斥量的各个函数:
#include <pthread.h>
/* 动态初始化互斥量 */
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
/* 静态初始化互斥量 */
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
/* 给互斥量上锁,会阻塞等待 */
int pthread_mutex_lock(pthread_mutex_t *mutex);
/* 尝试给互斥量上锁,不会阻塞等待,上不了立即返回 */
int pthread_mutex_trylock(pthread_mutex_t *mutex);
/* 给互斥量解锁 */
int pthread_mutex_unlock(pthread_mutex_t *mutex);
/* 销毁互斥量 */
int pthread_mutex_destroy(pthread_mutex_t *mutex);
静态初始化互斥量是使用的默认属性。lock 和 unlock 中间的区域被称为临界区。
下面我们使用互斥量来解决上面例子出现的问题,在 indexs++ 的执行在临界区中使其变为原子的操作:
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#define THREADNUM 10
static pthread_mutex_t mut = PTHREAD_MUTEX_INITIALIZER;
int indexs = 0;
void *thread_func(void *arg)
{
for (int i = 0; i < 10000000; i++) {
pthread_mutex_lock(&mut);
indexs++;
pthread_mutex_unlock(&mut);
}
pthread_exit(NULL);
}
int main()
{
pthread_t thread_ids[THREADNUM] = {0};
int err;
for (int i = 0; i < THREADNUM; i++) {
err = pthread_create(thread_ids + i, NULL, thread_func, NULL);
if (err) {
fprintf(stderr, "pthread_create");
exit(1);
}
}
for (int i = 0; i < THREADNUM; i++) {
if (thread_ids[i] != 0) {
pthread_join(thread_ids[i], NULL);
}
}
printf("%d\n", indexs);
pthread_mutex_destroy(&mut);
exit(0);
}
再次运行结果就是正确的了:
当然,想上面这种加锁方式非常的低效,因为每次执行 indexs++ 都要请求锁 -> 释放锁,并且 indexs++ 执行地非常频繁,这导致线程间的锁的竞争非常强烈(一个线程拿到了锁,其他线程要等待),后面在锁的细粒度会涉及到这部分内容。
另外一种同步方法是使用条件变量(pthread_cond_t)。
#include <pthread.h>
/* 静态初始化 */
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
/* 动态初始化 */
int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr);
/* 叫醒任意一个因为条件变量而阻塞的线程 */
int pthread_cond_signal(pthread_cond_t *cond);
/* 叫醒所有因为条件变量而阻塞的线程 */
int pthread_cond_broadcast(pthread_cond_t *cond);
/* 等待条件变量 */
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
/* 有限时间地等待条件变量 */
int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct
timespec *abstime);
/* 销毁条件变量 */
int pthread_cond_destroy(pthread_cond_t *cond);
pthread_cond_wait() 函数会先释放互斥锁 mutex,然后睡眠等待,直到被 pthread_cond_signal() 或者 pthread_cond_broadcast() 唤醒,然后抢锁,查看条件变量。
例子,primes_thread_pool_cond.c,使用条件变量与多线程实现质数计算:
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#define MAX 100
#define THREADNUM 4
static int num = 0; // the num that give to thread to compute
static pthread_mutex_t mut_num = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond_num = PTHREAD_COND_INITIALIZER;
void *thread_func(void *arg)
{
int i;
while (1) {
pthread_mutex_lock(&mut_num);
while (num == 0) {
pthread_cond_wait(&cond_num, &mut_num);
}
// all work has done, exit the thread
if (num == -1) {
pthread_mutex_unlock(&mut_num);
pthread_exit(arg);
}
i = num;
num = 0;
pthread_mutex_unlock(&mut_num);
// rouse main thread
pthread_cond_broadcast(&cond_num);
int flag = 1;
for (int j = 2; j <= i / 2; j++) {
if (i % j == 0) {
flag = 0;
break;
}
}
if (flag) {
printf("thread%d: %d\n", *(int*)arg, i);
}
}
fprintf(stderr, "error\n");
exit(1);
}
int main()
{
pthread_t thread_ids[THREADNUM] = {0};
int err;
void *retvalue;
for (int i = 0; i < THREADNUM; i++) {
int *ip = (int *)malloc(sizeof(int));
*ip = i;
err = pthread_create(thread_ids + i, NULL, thread_func, ip);
if (err) {
fprintf(stderr, "pthread_create");
exit(1);
}
}
// assign compute task to thread
for (int i = 2; i < MAX; i++) {
pthread_mutex_lock(&mut_num);
while (num != 0) {
pthread_cond_wait(&cond_num, &mut_num);
}
num = i;
pthread_mutex_unlock(&mut_num);
pthread_cond_signal(&cond_num);
}
// set num to -1, means all work has done
pthread_mutex_lock(&mut_num);
while (num != 0) {
pthread_cond_wait(&cond_num, &mut_num);
}
num = -1;
pthread_mutex_unlock(&mut_num);
for (int i = 0; i < THREADNUM; i++) {
if (thread_ids[i] != 0) {
pthread_join(thread_ids[i], &retvalue);
free(retvalue);
}
}
pthread_mutex_destroy(&mut_num);
pthread_cond_destroy(&cond_num);
exit(0);
}
但是条件变量只有 0 和 1,而信号量可以是任意数量,下面是使用互斥量和条件变量实现的信号量机制:
/* 信号量实现,可以实现某些固定的资源数量 */
#include <stdio.h>
#include <stdlib.h>
#include "mysem.h"
#include <pthread.h>
struct mysem_st
{
int value;
pthread_mute_t mute;
pthread_cond_t cond;
};
mysem_t *mysem_init(int initval)
{
struct mysem_st *me = (struct mysem_st *)malloc(sizeof(*me));
if (me == NULL) {
return NULL;
}
me->value = initval;
pthread_mute_init(&me->mute, NULL);
pthread_cond_init(&me->cond, NULL);
return me;
}
int mysem_add(mysem_t *sem, int num)
{
struct mysem_st *me = (struct mysem_st *)sem;
pthread_mute_lock(&me->mute);
me->value += num;
pthread_mute_unlock(&me->mute);
pthread_cond_broadcast(&me->cond);
return 0;
}
int mysem_sub(mysem_t *sem, int num)
{
struct mysem_st *me = (struct mysem_st *)sem;
pthread_mute_lock(&me->mute);
while (me->value < num) {
pthread_cond_wait(&me->cond, &me->mute);
}
me->value -= num;
pthread_mute_unlock(&me->mute);
}
int mysem_destroy(mysem_t *sem)
{
if (sem == NULL) {
return -1;
}
struct mysem_st *me = (struct mysem_st *)sem;
pthread_mute_destroy(&me->mute);
pthread_cond_destroy(&me->cond);
free(me);
return 0;
}
读写锁: 读锁 -> 共享锁,写锁 -> 互斥锁。
2.4 线程属性,线程同步的属性
线程属性的标识符为:pthread_attr_t。
相关的函数有:
#include <pthread.h>
/* 线程属性结构体初始化 */
int pthread_attr_init(pthread_attr_t *attr);
/* 线程属性结构体销毁 */
int pthread_attr_destroy(pthread_attr_t *attr);
除此之外,还有很多,比如 pthread_attr_setstack(3) 可以设置线程的栈大小:
线程间通信要比进程间通信快,因为线程共享同一个进程的地址空间。而进程间通信需要借助特殊的机制,比如管道(有名管道和匿名管道)、消息队列、共享内存、信号量(semaphore)、信号(signal)、socket。
除此之外还有互斥量和条件变量的属性。
clone(2) 系统调用可以创建子进程,和 fork 不同的是,clone 可以更加精细地指定父子进程之间的资源共享,比如共享文件描述符表,进程地址空间等。 通过指定一些资源的共享,clone 创建的进程比 fork 创建的子进程和 pthread_create 创建的线程更加灵活。
线程与信号:pthread_sigmask()、sigwait()、pthread_kill()。
2.5 openmp 线程标准(相对于 posix 线程标准)
借助编译器来实现并发。跨语言。