Files
Linux-C-Notes/C16-并发/C16-并发.md
2024-05-27 02:28:25 +08:00

9.7 KiB
Raw Blame History

并发

  • 同步
  • 异步:到来的时刻不确定

异步事件的处理:

  1. 查询法:事件发生的频率高,查询方式复杂
  2. 通知法:事件发生的频率低,通知方式简单

信号(初步异步)

信号的概念

信号是软件中断。 信号的响应依赖于中断。

信号的处理函数应该尽量短小,注意是否可重入问题。 使用系统调用不能轻易使用IO。

kill -l # 查看信号列表

core文件:错误现场的保存。 对于个人用户,默认core文件大小为0可通过ulimit -c设置。

signal()函数

#include <signal.h>

typedef void (*sighandler_t)(int);

/**
 * signal - 设置信号处理函数
 * @param: signum  信号编号
 * @param: handler 信号处理函数
 * 
 * 返回值:原信号处理函数
 */
sighandler_t signal(int signum, sighandler_t handler);

//* 不使用typedef, 直接声明函数原型
void (*signal(int signum,void (*func)(int)))(int);
// void (*signal(...))这表示signal函数的返回类型是一个指向函数的指针该函数返回类型为void。
// (int signum, void (*func)(int))这是signal函数的参数列表其中signum是一个整数类型的参数func是一个指向参数为整数类型、返回类型为void的函数的指针。
// (int):这个函数指针所指向的函数接受一个整数类型的参数。

信号会打断阻塞的系统调用。 writereadopenfork等系统调用都可能被信号打断。 故要判断返回值是不是EINTR,如果是,则表示信号被打断,需要重新调用。

信号的不可靠

在 Linux 中信号被用来通知进程发生了某些事件例如终端用户按下了中断键Ctrl+C或者一个进程运行时间过长等。但是信号被称为“不可靠”的主要是因为以下几个原因

  1. 丢失信号:在传统的 Unix 实现中,同一种信号类型如果在处理前多次发生,可能会被合并,只传递一次。这意味着除了第一次之外的其他信号都会被丢弃。例如,如果一个程序几乎同时收到两个 SIGINT 信号,它可能只能感知到一个。

  2. 不可预测的顺序:如果多个不同的信号几乎同时到达,它们被递送到进程的顺序可能与实际发生的顺序不同,这会使得程序的行为难以预测。

  3. 中断系统调用:在早期的 Unix 系统中,当信号被捕获时,正处于阻塞状态的系统调用(如 read, write, select 等)可能会被中断并提前返回,这常常需要额外的错误处理逻辑来重新发起系统调用。

  4. 异步性信号是异步的这意味着它们可以在程序执行的任何时刻到达。如果信号处理函数signal handler不够简单它可能在执行程序的中间阶段被调用而此时程序可能处于一个不一致的状态。因此信号处理函数需要非常小心地编写通常只能执行异步信号安全的函数。

为了克服信号的不可靠性,现代操作系统和库引入了新的机制,比如 sigaction API 允许更精细的控制信号处理,以及使用 pselectepoll 等函数的组合来避免系统调用被中断的问题。此外,一些高级语言或框架提供了更高层次的抽象,使得信号处理变得更为安全和可靠。

可重入函数

所有的系统调用都是可重入的,一部分库函数也是可重入的,比如memcpy

信号的响应过程

每个进程至少两个位图,maskpending一般都是32位的。 mask是当前进程阻塞的信号集合,pending是当前进程收到的信号集合。

mask && pending得到的是当前进程需要处理的信号集合。

信号从收到到响应有一个不可避免的延迟。

标准信号的响应没有严格的顺序。

屏蔽信号就是通过mask置位来屏蔽信号。

M P 1 0 常规状态 收到信号,未处理 1 1 从内核态回用户态M & P = 1处理信号 0 0 处理信号前M和P都置0 响应完后M置1再次对M & P进行判断。 有则重复无则将M置1回到常规状态。 1 0

不能从信号处理函数中随意的往外跳。setjmp, longjmp 这样错过了将M置1的机会。 标准sigsetjmp, siglongjmp, 推出这俩来保存掩码情况。

常用函数

/**
 * kill - 向进程发送信号
 * @param: pid: 进程ID:
 *            >0 : 进程ID
 *            =0 : 进程组ID (组内广播)
 *            =-1: 给当前进程有权限发信号的所有进程 (除了init进程)
 *            < -1: 发送给|pid|的进程 (eg. -5 就发送给 5号进程)
 *
 * @note: 
 *       这里的pid用法可以联系
 *       pid_t waitpid(int pid, int *status, int options)
 *
 * @param: sig: 信号编号
 *            =0 : 不发送信号error check用于检测一个进程或者进程组是否存在
 *
 * 返回值成功返回0失败返回-1并设置errno
 * errno:
 *       EINVAL : 参数错误
 *       EPERM  : 无权限发送信号
 *       ESRCH  : 进程不存在
 */
int kill(pid_t pid, int sig);

/**
 * raise - 发送信号给当前进程
 * @param: sig 信号编号
 *
 * 返回值成功返回0失败返回非零
 */
int raise(int sig);
kill(getpid(), SIGINT); // 等价
pthread_kill(pthread_self(), SIGINT); // 多线程等价

/**
 * alarm - 设置闹钟
 * @param: seconds: 闹钟时间,单位为秒
 *
 * @note
 *       - 闹钟时间到时会发送SIGALRM信号给当前进程。
 *       - 有的平台 sleep 是 alarm + pause 实现的
 *         这时sleep和alarm在同一个程序中会冲突
 *       - 流量控制的基础
 *
 * @eg: 使用单一计时器,构造一组函数,实现任意数量的定时器
 *      用到 alarm 或 setitimer
 *      TODO
 *
 * 返回值成功返回0失败返回非零
 */
unsigned int alarm(unsigned int seconds);

/**
 * pause - 挂起进程
 *
 * @note: 挂起进程,直到收到信号或被唤醒
 */
int pause(void);

/**
 * abort - 终止进程
 *
 * @note: 终止进程立即退出不执行清理函数得到core文件
 */
void abort(void);

/**
 * system - 执行系统命令
 * @param: command: 命令字符串
 * @note: 执行系统命令,阻塞当前进程,直到命令执行完毕
 *        ! 需要 block SIGCHLD 并且 ignore SIGINT SIGQUIT
 * @return: 命令执行的返回值
 */
int system(const char *command);

/**
 * sleep - 休眠进程
 * @param: seconds: 休眠时间,单位为秒
 *
 * @note: 休眠当前进程,直到指定的时间段结束
 *
 * @return: 剩余时间,单位为秒
 */
unsigned int sleep(unsigned int seconds);
// 可以封装出sleep函数
int nanosleep(const struct timespec *req, struct timespec *rem);
int usleep(useconds_t usec);
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

信号集

struct sigset_t;   // 信号集类型
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set, int signum);
int sigdelset(sigset_t *set, int signum);
int sigismember(const sigset_t *set, int signum);

信号屏蔽字/pending的处理

/**
 * sigprocmask - 设置信号掩码
 * @param: how: 掩码操作类型
 * @param: set: 信号集指针
 * @param: oldset: 旧的信号集指针
 */
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

/**
 * sigpending - 获取未决信号集
 * @param: set: 信号集指针
 * @note: 该函数获取当前进程的未决信号集
 *        该函数会清空pending信号集并将其复制到set指向的信号集中。
 *        该函数不修改信号掩码。
 * 
 * !!!: 没有使用场景,不推荐使用
 * 取到的信号是响应之前的信号,等取到的时候,情况可能已经变化。
 */
int sigpending(sigset_t *set);

扩展

/**
 * sigwait - 等待信号
 */
int sigsuspend(const sigset_t *mask);

/**
 * sigaction - 设置信号处理函数替代signal
 * @param: signum: 信号编号
 * @param: act: 信号处理函数
 * @param: oldact: 旧的信号处理函数
 *
 * @note: signal无法区分信号来自user还是kernel
 *        sigaction可以区分
 *        看siginfo_t.si_code可以区分
 */
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

struct sigaction {
    //!在某些结构, sa_handler和sa_sigaction只能有一个被设置
    //!因为他们是共用体
    void (*sa_handler)(int); /* 信号处理函数 */
    void (*sa_sigaction)(int, siginfo_t *, void *); /* 信号处理函数 */
    sigset_t sa_mask;        /* 信号掩码 */
    int sa_flags;             /* 信号标志 */
    void (*sa_restorer)(void); /* 信号状态恢复函数 */
}; 

/**
 * setitimer - 设置定时器替代alarm
 * @param: which: 定时器类型
 *            ITIMER_REAL    : 实时定时器
 *            ITIMER_VIRTUAL : 虚拟定时器
 *            ITIMER_PROF    : 进程计时器
 * @param: new_value: 新的定时器值
 * @param: old_value: 旧的定时器值
 *
 * @note: 原子赋值,比 alarm 更精细
 *
 * @return成功返回0失败返回-1并设置errno
 */
int setitimer(int                     which,
              const struct itimerval *new_value,
              struct itimerval       *old_value);

// 在it_value之后第一次启动
// 后续以it_interval为间隔启动
struct itimerval {
    struct timeval it_interval; /* next value */
    struct timeval it_value;    /* current value */
};

struct timeval {
    time_t tv_sec;     /* seconds */
    suseconds_t tv_usec; /* microseconds */
};

/**

实时信号

查看信号

  • kill -l
  • cat /usr/include/bits/signum-generic.h
  1. 排队
  2. 不丢失

此外都和标准信号一样

线程(强烈异步)