# 并发 - 同步 - 异步:到来的时刻不确定 异步事件的处理: 1. 查询法:事件发生的频率高,查询方式复杂 2. 通知法:事件发生的频率低,通知方式简单 ## 信号(初步异步) ### 信号的概念 > > 信号是软件中断。 信号的响应依赖于中断。 信号的处理函数应该尽量短小,注意是否可重入问题。 使用系统调用,不能轻易使用IO。 ```bash kill -l # 查看信号列表 ``` `core`文件:错误现场的保存。 对于个人用户,默认`core`文件大小为0,可通过`ulimit -c`设置。 ### `signal()`函数 ```c #include 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):这个函数指针所指向的函数接受一个整数类型的参数。 ``` **信号会打断阻塞的系统调用。** `write`,`read`,`open`,`fork`等系统调用都可能被信号打断。 故要判断返回值是不是`EINTR`,如果是,则表示信号被打断,需要重新调用。 ### 信号的不可靠 在 Linux 中,信号被用来通知进程发生了某些事件,例如终端用户按下了中断键(Ctrl+C),或者一个进程运行时间过长等。但是信号被称为“不可靠”的,主要是因为以下几个原因: 1. **丢失信号**:在传统的 Unix 实现中,同一种信号类型如果在处理前多次发生,可能会被合并,只传递一次。这意味着除了第一次之外的其他信号都会被丢弃。例如,如果一个程序几乎同时收到两个 `SIGINT` 信号,它可能只能感知到一个。 2. **不可预测的顺序**:如果多个不同的信号几乎同时到达,它们被递送到进程的顺序可能与实际发生的顺序不同,这会使得程序的行为难以预测。 3. **中断系统调用**:在早期的 Unix 系统中,当信号被捕获时,正处于阻塞状态的系统调用(如 read, write, select 等)可能会被中断并提前返回,这常常需要额外的错误处理逻辑来重新发起系统调用。 4. **异步性**:信号是异步的,这意味着它们可以在程序执行的任何时刻到达。如果信号处理函数(signal handler)不够简单,它可能在执行程序的中间阶段被调用,而此时程序可能处于一个不一致的状态。因此,信号处理函数需要非常小心地编写,通常只能执行异步信号安全的函数。 为了克服信号的不可靠性,现代操作系统和库引入了新的机制,比如 `sigaction` API 允许更精细的控制信号处理,以及使用 `pselect` 或 `epoll` 等函数的组合来避免系统调用被中断的问题。此外,一些高级语言或框架提供了更高层次的抽象,使得信号处理变得更为安全和可靠。 ### 可重入函数 所有的系统调用都是可重入的,一部分库函数也是可重入的,比如`memcpy` ### 信号的响应过程 每个进程至少两个位图,`mask`和`pending`,一般都是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, 推出这俩来保存掩码情况。 ### 常用函数 ```c /** * 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 * * 返回值:成功返回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); ``` ### 信号集 ```c 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`的处理 ```c /** * 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); ``` ### 扩展 ```c /** * 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. 不丢失 此外都和标准信号一样 ## 线程(强烈异步) ### 线程的概念 会话-->进程-->线程 一个正在运行的函数。 `main`函数可以叫做main线程,不应该说主线程,线程之间平等。 `posix`线程是一套标准,而不是实现。 `openmp`线程。 例如,线程标识: `pthread_t`, 具体类型怎么实现是不知道的 在linux环境,可以通过`ps axm`或者`ps -ax -L`查看线程信息。 发现线程消耗了进程号。 ```c #include //* Compile and link with -pthread /** * 比较两个pthread_t是否相等 * @param: t1: pthread_t * @param: t2: pthread_t * * @return: 相等返回非0,不相等返回0 */ int pthread_equal(pthread_t t1, pthread_t t2); /** * 获取当前线程的pthread_t * @return: 当前线程的pthread_t */ pthread_t pthread_self(void); ``` ### 线程的操作 创建,终止,取消,栈的清理 线程的调度取决于调度器策略。 ```c #include // Compile and link with -pthread /** * 创建线程 * @param: thread: 线程ID指针 * @param: attr: 线程属性指针 * NULL: 默认属性 * @param: start_routine: 线程函数指针 * void *(*)(void *) * @param: arg: 线程函数参数 * * @return: 成功返回0,失败返回errCode * 建议使用strerror而非perror */ int pthread_create(pthread_t * thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg); ``` ### 线程的同步 ### 线程属性,线程同步的属性 ### 重入,线程与信号,线程与`fork`