1、信号基本概念和操作
信号就是一条消息,通知进程系统中发生了什么事,每种信号都对应着某种系统事件。一般的底层硬件异常是由内核的异常处理程序处理的,它对用户进程来说是透明的。而信号机制,提供了一种方法通知用户进程发生了这些异常。
1.1 信号概念及种类
信号是 UNIX 中所使用的进程通信的一种最古老的方法。它是在软件层次上对中 断机制的一种模拟,是一种异步通信方式。信号可以直接进行用户空间进程和内核进 程之间的交互,内核进程也可以利用它来通知用户空间进程发生了哪些系统事件。它 可以在任何时候发给某一进程,而无需知道该进程的状态。如果该进程当前并未处于 执行态,则该信号就由内核保存起来,直到该进程恢复执行再传递给它为止;如果一 个信号被进程设置为阻塞,则该信号的传递被延迟,直到其阻塞被取消时才被传递给 进程。
信号就是一条消息,通知进程系统中发生了什么事,每种信号都对应着某种系统事件。一般的底层硬件异常是由内核的异常处理程序处理的,它对用户进程来说是透明的。而信号机制,提供了一种方法通知用户进程发生了这些异常。
例如,一个进程试图除0,会引发内核向他发送SIGFPE信号;执行非法指令会引发SIGILL信号;非法内存访问引发SIGSEGV;当你从键盘上键入Ctrl + C会引发SIGINT;当某个子进程结束会引发内核向其父进程发送SIGCHLD信号,等等。具体请看下表:
信号名称 | 默认行为 | 对应事件 |
---|---|---|
SIGHUP | 终止 | 终端线挂断:该信号在用户终端连接(正常或非正常)结束时发出,通常是在终 端的控制进程结束时,通知同一会话内的各个作业与控制终端不再关联。 |
SIGINT | 终止 | 来自键盘的中断Ctrl + C |
SIGQUIT | 终止 | 来自键盘的退出Ctrl + \ |
SIGILL | 终止 | 非法指令 |
SIGTRAP | 终止并转储内存 | 跟踪陷阱 |
SIGABRT | 终止并转储内存 | 来自abort函数的终止信号 |
SIGBUS | 终止 | 总线错误 |
SIGFPE | 终止并转储内存 | 浮点异常,这里不仅包括浮点运算 错误,还包括溢出及除数为 0 等其他所有的算术错误 |
SIGKILL | 终止 | 杀死程序,该信号用来立即结束程序的运行,并且不能被阻塞、处理或忽略 |
SIGUSR1 | 终止 | 用户定义的信号1 |
SIGSEGV | 终止并转储内存 | 无效的内存引用 |
SIGUSR2 | 终止 | 用户定义的信号2 |
SIGPIPE | 终止 | 向没有读用户的管道做写操作 |
SIGALRM | 终止 | 来自alarm函数的定时器信号,当一个定时器到时的时候发出。 |
SIGTERM | 终止 | 软件终止信号 |
SIGSTKFLT | 终止 | 协处理器上的栈故障 |
SIGCHLD | 忽略 | 一个子进程停止或终止 |
SIGCONT | 忽略 | 继续进程 |
SIGSTOP | 停止,直到下一个SIGCONT | 非终端的停止信号,用于暂停一个进程,且不能被阻塞、处理或忽略 |
SIGTSTP | 停止,直到下一个SIGCONT | 终端的停止信号,用于交互停止进程,用户键入Ctrl+Z 时发出这个信号 |
SIGTTIN | 停止,直到下一个SIGCONT | 后台进程从终端读 |
SIGTTOU | 停止,直到下一个SIGCONT | 后台进程从终端写 |
SIGURG | 忽略 | 套接字上的紧急情况 |
SIGXCPU | 终止 | CPU时间限制超出 |
SIGXFSZ | 终止 | 文件大小限制超出 |
SIGVTALRM | 终止 | 虚拟定时器满 |
SIGPROF | 终止 | 剖析定时器满 |
SIGWINCH | 忽略 | 窗口大小变化 |
SIGIO | 终止 | 在某个描述符上可执行IO |
SIGPWR | 终止 | 电源故障 |
- 可靠信号 VS 不可靠信号
- 命令
kill -l
可以列出系统所支持的所有信号的列表。在笔者的系统中,信号值在 32 之前的则有不同的名称,而信号值在 32 以后的都是用“SIGRTMIN”或“SIGRTMAX”开头的,这就是两类典型的信号。- 前者是从 UNIX 系统中继承下来的信号,为不可靠信号(也称为非实时信号);
- 后者是为了解决前面“不可靠信号”的问题而进行了更改和扩充的信号, 称为“可靠信号”(也称为实时信号)。
- 命令
- 信号的生命周期
- 一个完整的信号生命周期可以分为 3 个重要阶段,这 3 个阶段由 4 个重要事 件来刻画的:信号从内核进程中产生、信号在用户进程中注册、信号在用户进程中注销、执行信号处理函数。
- 一个不可靠信号的处理过程是这样的:如果发现该信号已经在进程中注册, 那么就忽略该信号。因此,若前一个信号还未注销又产生了相同的信号就会产 生信号丢失。
- 一个可靠信号发送给一个进程时,不管该信号是否已经在进程中 注册,都会被再注册一次,因此信号就不会丢失。所有可靠信号都支持排队, 而所有不可靠信号都不支持排队。
这里信号的产生、注册和注销等是指信号的内部实现机制,而不是调用信号的 函数实现。因此,信号注册与否,与本节后面讲到的发送信号函数(如 kill() 等)以及信号安装函数(如 signal()等)无关,只与信号值有关。
1.2信号发送
当内核检测到某种系统事件(除零错误或子进程终止等等)或一个进程调用了kill函数显式的要求内核发送一个信号给目的进程时,内核会通过更新目的进程上下文中的某个状态而达到向它发送一个信号的目的。
发送信号既可以通过命令行、键盘也可以通过函数,其中信号发送函数主要有 kill()、raise()、alarm()以及 pause(),下面就依次对其发送信号的方式为:
-
**命令行:**用
kill -signum PID
命令,向进程号为PID的进程发送signum信号; -
**键盘:**通过键盘发送特定信号,Ctrl + C 向前台进程组中的每个进程发送SIGINT终止信号;Ctrl + Z 向前台进程组中的每个进程发送SIGTSTP暂停信号;Ctrl + \ 向前台进程组中的每个进程发送SIGQUIT退出信号。
-
alarm() :称为闹钟函数,它可以在进程中设置一个定时器,当定时器指定的时间到时,内核就向进程发送 SIGALARM 信号。注意:一个进程只能同时有一个闹钟,如果在调用 alarm()之前已设置过闹钟,则任何以前的闹钟都被新闹钟所代替。
- 原型:
unsigned int alarm(unsigned int secs)
- 参数:secs:系统经过 seconds 秒之后向该进程发送 SIGALRM 信号;
- 返回值:
- 成功:如果调用此 alarm()前,进程中已经设置了闹钟时间,则返回上一个闹钟时间的剩余时间,否则返回 0。
- 失败:-1
- 原型:
-
pause():用于将调用进程挂起,直至捕捉到信号为止。这个函数很常用,通常 可以用于判断信号是否已到。
- 原型:
int pause(void)
- 返回值:-1,并且把 error 值设为 EINTR
- 原型:
-
kill():进程通过调用kill函数发送信号给其它进程(包括自己)或进程组。
#include <sys/types.h> #include <signal.h> int kill(pid_t pid, int sig); //成功返回0,失败返回-1。
- pid > 0 :发送信号sig给进程pid;
- pid = 0 :发送信号给所在进程组中的每个进程,包括自己。
- pid = -1 :信号发给所有的进程表中的进程(除了进程号最大的进程外)
- pid < -1 :发送信号sig给进程组号为-pid的每一个进程。
-
raise():进程向自身发送信号,等效于
kill(getpid(), signo)
。返回值意义同kill函数。- 原型:
int raise(int signo)
- 参数:待发送的信号
- 返回值:
- 成功:0
- 失败:-1
- 原型:
1.3 信号处理
一 个进程可以决定在该进程中需要对哪些信号进行什么样的处理。例如,一个进程可以 选择忽略某些信号而只处理其他一些信号,另外,一个进程还可以选择如何处理信号。 总之,这些都是与特定的进程相联系的。因此,首先就要建立进程与其信号之间的对 应关系,这就是信号的处理。
当进程从系统调用返回或是完成了一次上下文切换而重新取得控制权之前,内核会检查该进程的待处理信号集(pengding&(~blocked)),如果为空则完成控制权的交接,如果不为空则会让进程响应该信号集合中信号值最小的那个信号。
目的进程收到信号后有“忽略信号”、“终止进程”和“捕获信号“这3种方式来响应。其中SIGKILL(终止)和SIGSTOP(暂停)这2个信号不可被忽略,也不能像其它信号一样可以通过signal函数改变他们的默认处理函数;
信号处理的主要方法有两种,一种是使用简单的 signal()函数,另一种是使用信号集函数组。下面首先介绍signal()函数处理方式:
1.3.1 信号处理函数
-
signal()
-
使用 signal()函数处理时,只需要指出要处理的信号和处理函数即可。它主要是用于前 32 种非实时信号的处理,不支持信号传递信息,但是由于使用简单、易于理解, 因此也受到很多程序员的欢迎。 Linux 还支持一个更健壮、更新的信号处理函数 sigaction(),推荐使用该函数。
-
原型:
void (*signal(int signum, void (*handler)(int)))(int)
typedef void (*sighandler_t)(int); sighandler_t signal(int signum, sighandler_t handler);
-
参数:
- signum:指定信号
- handler:分配给前面指定信号的处理函数
- SIG_IGN:忽略signum的信号;
- SIG_DFL:采用signum的信号的默认行为;
- 其它:用户自定义的信号处理函数地址。
-
-
sigaction()
-
原型:
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact)
-
参数:
- signum:除 SIGKILL 及 SIGSTOP 外的任何一个特定信号;
- act:指向结构 sigaction 的一个实例的指针,指定对特定信号的处理;
- oldact:保存原来对相应信号的处理;
-
返回值:
- 成功:0
- 失败:-1
-
sigaction结构体
-
定义:
struct sigaction { void (*sa_handler)(int signo); sigset_t sa_mask; int sa_flags; void (*sa_restore)(void); }
-
参数:
- sa_handler:信号处理函数,可以是SIG_DFL(默认动作)、SIG_IGN(忽略)或用户自定义的函数;
- sa_mask:在信号处理程序执行期间应当被屏蔽的信号集;
- sa_flag:对信号进行处理时的各个选择项
- SA_NODEFER、SA_NOMASK:当捕捉到此信号时,在执行其信号捕捉函数时,系统不会自动屏蔽此信号;
- SA_NOCLDSTOP:进程忽略子进程产生的任何 SIGSTOP、SIGTSTP、SIGTTIN 和 SIGTTOU 信号;
- SA_RESTART:令重启的系统调用起作用;
- SA_ONESHOT、SA_RESETHAND:自定义信号只执行一次,在执行完毕后恢复信号的系统默认动作。
-
-
-
示例:my_func 是信号处理 的函数指针。读者还可以将其改为 SIG_IGN 或 SIG_DFL 查看运行结果
-
使用 signal()函数捕捉相应信号:
#include <signal.h> #include <stdio.h> #include <stdlib.h> /*自定义信号处理函数*/ void my_func(int sign_no) { if (sign_no == SIGINT) { printf("I have get SIGINT\n"); } else if (sign_no == SIGQUIT) { printf("I have get SIGQUIT\n"); } } int main() { printf("Waiting for signal SIGINT or SIGQUIT...\n"); /* 发出相应的信号,并跳转到信号处理函数处 */ signal(SIGINT, my_func); signal(SIGQUIT, my_func); pause(); exit(0); }
-
运行调试:
$ ./signal Waiting for signal SIGINT or SIGQUIT... I have get SIGINT //(按 ctrl-c 组合键) $ ./signal Waiting for signal SIGINT or SIGQUIT... I have get SIGQUIT //(按 ctrl-\ 组合键)
-
-
使用 sigaction()函数捕捉相应信号:
/* sigaction.c */ /* 前部分省略 */ ... int main() { struct sigaction action; printf("Waiting for signal SIGINT or SIGQUIT...\n"); /* sigaction 结构初始化 */ action.sa_handler = my_func; sigemptyset(&action.sa_mask); action.sa_flags = 0; /* 发出相应的信号,并跳转到信号处理函数处 */ sigaction(SIGINT, &action, 0); sigaction(SIGQUIT, &action, 0); pause(); exit(0); }
-
1.3.2 信号集函数组
-
使用信号集函数组处理信号时涉及一系列的函数,这些函数按照调用的先后次序可分为以下几大功能模 块:创建信号集合、注册信号处理函数以及检测信号。
- 创建信号集合主要用于处理用户感兴趣的一些信号,其函数包括以下几个:
int sigemptyset(sigset_t *set); //初始化set集为空 int sigfillset(sigset_t *set); //将所有信号都添加进set集 int sigaddset(sigset_t *set, int signum);//将signum加入到信号集合set中去 int sigdelset(sigset_t *set, int signum);//将signum从信号集合set中去掉 int sigismember(const sigset_t *set, int signum);//查询指定信号是否在信号集合之中 返回值: 成功:0(sigismember 成功返回 1,失败返回 0 失败:-1
-
注册信号处理函数主要用于决定进程如何处理信号。这里要注意的是,信号集里的信号并不是真正可 以处理的信号,只有当信号的状态处于非阻塞(屏蔽)状态时才会真正起作用。因此,首先要使用 sigprocmask() 函数检测并更改信号屏蔽字(信号屏蔽字是用来指定当前被阻塞的一组信号,它们不会被进程接收), 然后才使用 sigaction()函数来定义进程接收到特定信号之后的行为。
int sigprocmask(int HOW, const sigset_t *set, sigset_t *oldset);
- 参数:
- HOW:决定函数的操作方式
- SIG_BLOCK:把set集中的信号加到进程的阻塞集合blocked中(blocked |= set);
- SIG_UNBLOCK:从进程的blocked中删除set集中的信号(blocked &= ~set);
- SIG_SETMASK:将当前进程的blocked设置为信号集合set(blocked = set);
- set:指定信号集
- oldset:进程原先的信号屏蔽字
- HOW:决定函数的操作方式
- 返回值:
- 成功:0
- 失败:-1
- 参数:
-
检测信号是信号处理的后续步骤,因为被阻塞的信号不会传递给进程,所以这些信号就处于“未处理”状态(也就是进程不清楚它的存在)。 sigpending()函数允许进程检测“未处理”信号,并进一步决定对它们作何处理。
int sigpending(sigset_t *set);
- 参数:set为要检测的信号集合
- 返回值:成功返回0,失败返回-1。
以下示例展示了临时忽略SIGINT信号的程序片段:
sigset_t mask, oldmask; sigemptyset(&mask); sigaddset(&mask, SIGINT); sigprocmask(SIG_BLOCK, &mask, &oldmask); . . //此处的所有语句将不会响应SIGINT信号 . sigprocmask(SIG_SETMASK, &oldmask, NULL); //之后的语句将会正常响应SIGINT信号
-
任何信号只能被记录阻塞一次;即如果进程正在执行某类型信号的处理函数,那么在此进程返回主程序前,不管又收到了多少个该类型的信号,它只会被记录一次(即等到该进程从上次处理函数返回后,它只会再响应一次该类型的信号)。因为内核为每个进程在pending位向量中维护着待处理信号的集合,而在blocked位向量中维护着被阻塞的信号集。每次,收到一个信号,就在blocked相应的位置1,响应一个信号,就在pengding中相应的清零。
示例:首先把 SIGQUIT、SIGINT 两个信号加入信号集,然后将该信号集合设为阻塞状态,并进入用户输 入状态。用户只需按任意键,就可以立刻将信号集合设置为非阻塞状态,再对这两个信号分别操作,其中 SIGQUIT 执行默认操作,而 SIGINT 执行用户自定义函数的操作。
/* sigset.c */
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
/*自定义的信号处理函数*/
void my_func(int signum)
{
printf("If you want to quit,please try SIGQUIT\n");
}
int main()
{
sigset_t set,pendset;
struct sigaction action1,action2;
/* 初始化信号集为空 */
if (sigemptyset(&set) < 0)
{
perror("sigemptyset");
exit(1);
}
/* 将相应的信号加入信号集 */
if (sigaddset(&set, SIGQUIT) < 0)
{
perror("sigaddset");
exit(1);
}
if (sigaddset(&set, SIGINT) < 0)
{
perror("sigaddset");
exit(1);
}
if (sigismember(&set, SIGINT))
{
sigemptyset(&action1.sa_mask);
action1.sa_handler = my_func;
action1.sa_flags = 0;
sigaction(SIGINT, &action1, NULL);
}
if (sigismember(&set, SIGQUIT))
{
sigemptyset(&action2.sa_mask);
action2.sa_handler = SIG_DFL;
action2.sa_flags = 0;
sigaction(SIGQUIT, &action2,NULL);
}
/* 设置信号集屏蔽字,此时 set 中的信号不会被传递给进程,暂时进入待处理状态 */
if (sigprocmask(SIG_BLOCK, &set, NULL) < 0)
{
perror("sigprocmask");
exit(1);
}
else
{
printf("Signal set was blocked, Press any key!");
getchar();
}
/* 在信号屏蔽字中删除 set 中的信号 */
if (sigprocmask(SIG_UNBLOCK, &set, NULL) < 0)
{
perror("sigprocmask");
exit(1);
}
else
{
printf("Signal set is in unblock state\n");
}
while(1);
exit(0);
}
1.3.3 安全的信号处理函数
由于信号处理函数和主程序是并发运行的,他们享有相同的全局变量,他们的运行顺序是不可预测的,这就导致何时接收到信号的规则往往有违人们的直觉,或者说主程序和子程序间不一定会按照你预想的顺序去执行。所以为了防止竞争冒险,在编写信号处理函数时有几个保守的原则需要遵守:
- 处理程序尽可能简单;
- 在处理程序中仅使用异步安全的函数,也就是说该函数是可重入的(只访问局部变量)且不能中断;下图列出了所有Linux保证安全的系统函数,可以发现许多常见的库函数(printf、sprintf、malloc、exit等)都不是安全函数,在编写信号处理函数时要尽量避免使用。
为了在信号处理程序中能够打印一些简单的消息,我们可以使用一些异步信号安全的系统函数来构建自己的特有包装函数。作为例子,下面的程序展示了利用异步信号安全的系统函数write编写自己的SIO(safe I/O)函数。
ssize_t sio_puts(char s[]) { int count = 0; char *str = s; if(!str) _exit(1); while(*str++) count++; return write(STDOUT, s, count); }
-
保存和恢复error;为了避免处理程序中某些语句的出错导致error被设置,进而影响主程序中的判断,在信号处理程序的第一条语句保存原error,在它返回前恢复error。
-
不管是主程序还是子程序,在访问全局变量时,都要阻塞所有的信号,以防相互干扰。
-
用volatile声明全局变量。 volatile要求编译器每次都是从内存中读取全局变量的值,而非从缓存中。
-
使用sig_atomic_t声明标志。 此处的标志代表在主程序和子程序间传递信号的全局变量,因为sig_atomic_t要求编译器对它的操作是原子的,所以即使没有阻塞所有信号,它也不会被任何信号打断。
-
使用sigaction函数重新包装signal函数,使得系统自动重启被中断的系统调用。 由于一些系统函数(例如read、write、accept等)需要执行较长时间,所以可能会被信号中断。而在许多较早以前版本的Unix系统中,被中断的系统调用并不会在信号处理返回后重启,而是直接返回错误并将error设置为EINTR。而sigaction函数可以设置信号处理时的语义。
以下代码用sigaction函数编写了signal函数的包装函数[Signal][1],并且具有如下语义:
- 只有当前处理的该类型信号被阻塞;
- 其它信号也不会排队等待;
- 只要可能,被中断的系统调用会自动重启;
- 一旦为某信号设置了信号处理程序,它会一直保持到Signal重新为该信号设置SIG_IGN或SIG_DFL的信号处理程序。
handler_t *Signal(int signum, handler_t *handler) { struct sigaction action,oldaction; action.sa_handler = handler; sigemptyset(&action.sa_mask); action.sa_flags = SA_RESTART; if(sigaction(signum, &action, &oldaction) < 0) unix_error("Signal error"); return(oldaction.sa_handler) }
2.信号的同步
当需要编写读写相同内存位置的并发进程,我们不得不考虑进程间的(既包括进程与进程之间,也包括主进程与子进程之间)竞争关系。这是一个很大的命题,在此限于文章主题,只讨论信号之间的竞争关系如何处理。主要分两个方面,一是隐式竞争,二是显式竞争。
2.1 避免隐式竞争
考虑一个类似shell的函数功能,父进程在一个全局作业列表中记录着它的当前子进程,每个作业一个条目。addjob和deletejob函数分别向这个作业列表中添加和删除作业。父进程每创建一个子进程就把它添加在作业列表中,每当在SIGCHLD信号处理程序中回收一个僵死的子进程时,就在job列表中删除这个子进程。
void handler(int sig)
{
int olderrno = errno; //保存进程的原error值
sigset_t mask_all,prev_all;
pid_t pid;
sigfillset(&mask_all); //将所有信号添加到信号集mask_all中
while((pid = waitpid(-1, NULL, 0)) > 0){ //回收僵死子进程
sigprocmask(SIG_BLOCK, &mask_all, prev_all); //阻塞(屏蔽)所有信号
deletejob(pid); //从job列表中删除僵死的子进程条目
sigprocmask(SIG_SETMASK, &prev_all, NULL);
}
if(errno != ECHILD) //如果父进程的所有子进程都已经回收,则内核发送ECHILD错误
Unix_error("waitpid error");
errno = olderrno; //恢复进程的原error值
}
int main(int argc, char **argv)
{
int pid;
sigset_t mask_all,mask_one,prev_one;
sigfillset(mask_all);
sigemptyset(mask_one);
sigaddset(&mask_one, SIGCHLD);
Signal(SIGCHLD, handler); //使用安全的Signal函数设置处理函数
initjobs(); //初始化工作列表
while(1){
/*在产生子进程前屏蔽SIGCHLD,以防止主进程还没执行到addjob就已经收到了因子进程终止
而发来的SIGCHLD信号,进而进入handler导致在jobs中找不到要删除的子进程条目*/
sigprocmask(SIG_BLOCK, &mask_one, &prev_one); //频闭SIGCHLD信号
if((pid = fork()) == 0){
sigprocmask(SIG_SETMASK, &prev_one, NULL); //子进程解除频闭SIGCHLD
execve("/bin/date", argv, NULL);
}
sigprocmask(SIG_BLOCK, &mask_all, NULL); //父进程屏蔽所有信号
addjob(pid);
sigprocmask(SIG_SETMASK, &prev_one, NULL); //父进程解除屏蔽
}
exit(0);
}
2.2 避免显式竞争
有时候主程序需要显式地等待某个信号处理运行。例如shell程序,它必须等待当前的前台进程结束,被SIGCHLD处理程序回收之后,才能继续创建另一个进程。主进程在等待的这段时间应该干些什么才最好呢?我们可以用一个无限循环语句,让主进程就在那执行。但这样也太浪费CPU的资源了;我们也可以用一个sleep或者nanosleep函数让主进程休眠,但到底休眠多长时间不好把握,间隔太小同样会造成多次循环,间隔太大,程序又会太慢。
合适的解决办法是,引入sigsuspend函数:
#include <signal.h>
int sigsuspend(const sigset_t *mask); //返回-1
它暂时挂起调用它的进程,利用参数mask替换当前的信号阻塞集,直到收到一个信号并进入处理程序(如果是终止信号,就直接返回),处理完之后返回主进程,并恢复原来的阻塞集。
下面例子展示了主进程在创建完子进程后,如何利用该函数显式的等待SIGCHLD的到来,以达到同步的效果。
#include <signal.h>
volatile sig_atomic_t pid;
void sigchld_handler(int signum)
{
int olderror = errno;
pid = waitpid(-1, NULL, 0);
int errno = olderrno;
}
void sigint_handler(int signum)
{
}
int main(int argc, char **argv)
{
sigset_t mask,prev;
Signal(SIGCHLD, sigchld_handler);
Signal(SIGINT, sigint_handler);
sigemptyset(&mask);
sigaddset(&mask, SIGCHLD);
while(1){
sigprocmask(SIG_BLOCK, &mask, &prev); //屏蔽SIGCHLD信号
if(fork() == 0) //子进程
exit(0);
pid = 0;
while(!pid){
sigsuspend(&prev); //挂起并等待SIGCHLD信号的到来,其处理函数会使得pid大于0
}
sigprocmask(SIG_SETMASK, &prev, NULL);
printf("...");
}
exit(0);
}
[1]: 引用:Unix Network Programming: The Sockets Networking API,第三版,第一卷
获取更多知识,请点击关注:
嵌入式Linux&ARM
CSDN博客
简书博客
知乎专栏