进程
常用命令
pstree # 查看进程树状图
ps -aux # 查看所有用户的进程,-ux查看当前用户进程
进程状态
| 状态 | 说明 |
|---|---|
| R | 运行状态。严格来说,应该是"可运行状态",即表示进程在运行队列中,处于正在执行或即将运行状态,只有在该状态的进程才可能在 CPU 上运行,而同一时刻可能有多个进程处于可运行状态。 |
| S | 可中断的睡眠状态。处于这个状态的进程因为等待某种事件的发生而被挂起,比如进程在等待信号。 |
| D | 不可中断的睡眠状态。通常是在等待输入或输出(I/O)完成,处于这种状态的进程不能响应异步信号。 |
| T | 停止状态。通常是被shell的工作信号控制,或因为它被追踪,进程正处于调试器的控制之下。 |
| Z | 退出状态。进程成为僵尸进程。 |
| X | 退出状态。进程即将被回收。 |
| s | 进程是会话其首进程。 |
| l | 进程是多线程的。 |
| + | 进程属于前台进程组。 |
| < | 高优先级任务。 |
创建进程
所有进程都基于一个父进程分叉出来,最初始的父进程为init。
system()
system()用于在程序中调用 shell 执行一条命令,内部通常会通过 fork() 创建子进程,再由子进程调用 exec() 执行 /bin/sh -c command。调用者会等待命令执行结束,并获得其退出状态。
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
pid_t result;
printf("This is a system demo!\n\n");
/*调用 system()函数*/
result = system("ls -l");
printf("Done!\n\n");
return result;
}
fork()
fork() 用于创建一个新的进程,调用后会产生父进程和子进程两个执行流。子进程会复制父进程的地址空间,返回值在父进程中为子进程 PID,在子进程中为 0,失败时返回 -1。
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
pid_t result;
printf("This is a fork demo!\n\n");
/*调用 fork()函数*/
result = fork();
/*通过 result 的值来判断 fork()函数的返回情况,首先进行出错处理*/
if(result == -1) {
printf("Fork error\n");
}
/*返回值为 0 代表子进程*/
else if (result == 0) {
printf("The returned value is %d, In child process!! My PID is %d\n\n", result, getpid());
}
/*返回值大于 0 代表父进程*/
else {
printf("The returned value is %d, In father process!! My PID is %d\n\n", result, getpid());
}
return result;
}
exec()
exec() 系列函数用于在当前进程中加载并执行新的程序映像,调用成功后不会返回,原进程的代码、数据和堆栈都会被新程序替换。常见接口有 execl、execv、execle、execve 等,失败时返回 -1 并设置 errno。
int main(void)
{
int err;
printf("this is a execl function test demo!\n\n");
err = execl("/bin/ls", "ls", "-la", NULL);
if (err < 0) {
printf("execl fail!\n\n");
}
printf("Done!\n\n");
}
exec族有六个函数。
fork+exec的示例
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main(void)
{
pid_t pid = fork();
if (pid < 0) {
perror("fork failed");
return 1;
}
if (pid == 0) {
// 子进程:用 ls 程序替换当前进程
execl("/bin/ls", "ls", "-l", NULL);
// exec 执行失败才会运行到这里
perror("exec failed");
return 1;
} else {
// 父进程:等待子进程结束
wait(NULL);
printf("child process finished\n");
}
return 0;
}
这段程序先通过 fork() 创建子进程。
子进程中调用 execl() 执行 /bin/ls -l,此时子进程原来的代码会被 ls 程序替换。
父进程调用 wait() 等待子进程执行结束,避免产生僵尸进程。
终止进程
正常终止
- 从main函数返回。
- 调用exit()函数终止。
- 调用_exit()函数终止。
异常终止
- 调用abort()函数异常终止。
- 由系统信号终止。
等待进程
wait()
wait() 用于父进程等待任意一个子进程结束,并回收其资源,避免产生僵尸进程。调用后父进程会阻塞,直到有子进程退出或收到信号;若传入 status,可获取子进程退出状态。
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main()
{
pid_t pid, child_pid;
int status;
pid = fork(); //(1)
if (pid < 0) {
printf("Error fork\n");
}
/*子进程*/
else if (pid == 0) { //(2)
printf("I am a child process!, my pid is %d!\n\n",getpid());
/*子进程暂停 3s*/
sleep(3);
printf("I am about to quit the process!\n\n");
/*子进程正常退出*/
exit(0); //(3)
}
/*父进程*/
else { //(4)
/*调用 wait,父进程阻塞*/
child_pid = wait(&status); //(5)
/*若发现子进程退出,打印出相应情况*/
if (child_pid == pid) {
printf("Get exit child process id: %d\n",child_pid);
printf("Get child exit status: %d\n\n",status);
} else {
printf("Some error occured.\n\n");
}
exit(0);
}
}
- WIFEXITED(status) :如果子进程正常结束,返回一个非零值
- WEXITSTATUS(status): 如果WIFEXITED非零,返回子进程退出码
- WIFSIGNALED(status) :子进程因为捕获信号而终止,返回非零值
- WTERMSIG(status) :如果WIFSIGNALED非零,返回信号代码
- WIFSTOPPED(status): 如果子进程被暂停,返回一个非零值
- WSTOPSIG(status): 如果WIFSTOPPED非零,返回一个信号代码
waitpid()
waitpid() 用于等待指定的子进程状态变化,原型为 pid_t waitpid(pid_t pid, int *status, int options);。pid 可指定等待某个子进程,options 可设置为 WNOHANG 实现非阻塞等待,返回值为子进程 PID、0 或 -1。wait()是waitpid()的特例。
- pid:参数pid为要等待的子进程ID,其具体含义如下:
- pid < -1:等待进程组号为pid绝对值的任何子进程。
- pid = -1:等待任何子进程,此时的waitpid()函数就等同于wait()函数。
- pid = 0:等待进程组号与目前进程相同的任何子进程, 即等待任何与调用waitpid()函数的进程在同一个进程组的进程。
- pid > 0:等待指定进程号为pid的子进程。
- wstatus:与wait()函数一样。
- options:参数options提供了一些另外的选项来控制waitpid()函数的行为。 如果不想使用这些选项,则可以把这个参数设为0。
- WNOHANG:如果pid指定的子进程没有终止运行,则waitpid()函数立即返回0, 而不是阻塞在这个函数上等待;如果子进程已经终止运行,则立即返回该子进程的进程号与状态信息。
- WUNTRACED:如果子进程进入了暂停状态(可能子进程正处于被追踪等情况),则马上返回。
- WCONTINUED:如果子进程恢复通过SIGCONT信号运行,也会立即返回(这个不常用,了解一下即可)。
信号
基本概念
概述
信号(signal)又称软中断信号。使用"生成(raise)“表示信号产生,使用"捕获(catch)“表示信号接收。信号是进程间唯一的异步通信机制。
系统支持的信号
cat@lubancat ~> kill -l
HUP INT QUIT ILL TRAP ABRT BUS FPE KILL USR1 SEGV USR2 PIPE ALRM TERM STKFLT
CHLD CONT STOP TSTP TTIN TTOU URG XCPU XFSZ VTALRM PROF WINCH POLL PWR SYS
| 信号值 | 名称 | 描述 | 默认处理方式 |
|---|---|---|---|
| 1 | SIGHUP | 控制终端关闭时产生。 | 终止 |
| 2 | SIGINT | 程序终止(interrupt)信号,在用户键入INTR字符(通常是Ctrl + C)时发出,用于通知前台进程组终止进程。 | 终止 |
| 3 | SIGQUIT | SIGQUIT和SIGINT功能类似,但SIGQUIT由QUIT字符(通常是Ctrl + \)来控制,进程在收到SIGQUIT退出时会产生core文件,类似于产生一个程序错误信号。 | 终止并产生转储文件(core文件) |
| 4 | SIGILL | CPU检测到某进程执行了非法指令时产生。通常是因为可执行文件本身出现错误, 或者试图执行数据段、堆栈溢出时也有可能产生这个信号。 | 终止并产生转储文件(core文件) |
| 5 | SIGTRAP | 由断点指令或其它trap指令产生,由debugger使用。 | 终止并产生转储文件(core文件) |
| 6 | SIGABRT | 调用系统函数 abort()时产生。 | 终止并产生转储文件(core文件) |
| 7 | SIGBUS | 总线错误时产生。一般是非法地址,包括内存地址对齐(alignment)出错。比如访问一个四个字长的整数,但其地址却不是4的倍数的错误。它与SIGSEGV的区别在于后者是由于对合法存储地址的非法访问触发的(如访问不属于自己存储空间或已经释放的存储空间)。 | 终止并产生转储文件(core文件) |
| 8 | SIGFPE | 处理器出现致命的算术运算错误时产生,不仅包括浮点运算错误,还包括溢出及除数为0等其它所有的算术的错误。 | 终止并产生转储文件(core文件) |
| 9 | SIGKILL | 系统杀戮信号。用来立即结束程序的运行,本信号不能被阻塞、处理和忽略。如果管理员发现某个进程终止不了,可尝试发送这个信号将进程杀死。 | 终止 |
| 10 | SIGUSR1 | 用户自定义信号。 | 终止 |
| 11 | SIGSEGV | 访问非法内存时产生,进程试图访问未分配给自己的内存,或试图往没有写权限的内存地址写数据。 | 终止 |
| 12 | SIGUSR2 | 用户自定义信号。 | 终止 |
| 13 | SIGPIPE | 这个信号通常在进程间通信产生,比如采用FIFO(管道)通信的两个进程,读管道没打开或者意外终止就往管道写,写进程会收到SIGPIPE信号。此外用Socket通信的两个进程,写进程在写Socket的时候,读进程已经终止,也会产生这个信号。 | 终止 |
| 14 | SIGALRM | 定时器到期信号,计算的是实际的时间或时钟时间,alarm函数使用该信号。 | 终止 |
| 15 | SIGTERM | 程序结束(terminate)信号,与SIGKILL不同的是该信号可以被阻塞和处理。通常用来要求程序自己正常退出,shell命令kill缺省产生这个信号,如果进程终止不了,才会尝试使用SIGKILL信号来终止。 | 终止 |
| 16 | SIGSTKFLT | 已废弃。 | 终止 |
| 17 | SIGCHLD | 子进程暂停或终止时产生,由父进程接收这个信号,如果父进程没有处理这个信号,也没有等待(wait)子进程,子进程虽然终止,但是还会在内核进程表中占有表项,这时的子进程称为僵尸进程,我们应该避免出现这种情况。父进程默认是忽略SIGCHILD信号的,但我们可以捕捉它,做成异步等待它派生的子进程终止,或者父进程先终止,这时子进程的终止自动由init进程来接管。 | 忽略 |
| 18 | SIGCONT | 系统恢复运行信号,让一个已经停止(stopped)的进程继续执行,本信号不能被阻塞,可以用一个handler来让程序在由stopped状态变为继续执行时完成特定的工作 | 恢复运行 |
| 19 | SIGSTOP | 系统暂停信号,用于停止进程的执行。注意它和terminate以及interrupt的区别:该进程还未结束,只是暂停执行,本信号不能被阻塞,处理或忽略。 | 暂停 |
| 20 | SIGTSTP | 由控制终端发起的暂停信号,用于停止进程的运行,但该信号可以被处理和忽略,比如用户键入SUSP字符时(通常是Ctrl+Z)发出这个信号。 | 暂停 |
| 21 | SIGTTIN | 后台进程发起输入请求时控制终端产生该信号。 | 暂停 |
| 22 | SIGTTOU | 后台进程发起输出请求时控制终端产生该信号。 | 暂停 |
| 23 | SIGURG | 套接字上出现紧急数据时产生。 | 忽略 |
| 24 | SIGXCPU | 处理器占用时间超出限制值时产生。 | 终止并产生转储文件(core文件) |
| 25 | SIGXFSZ | 文件尺寸超出限制值时产生。 | 终止并产生转储文件(core文件) |
| 26 | SIGVTALRM | 由虚拟定时器产生的虚拟时钟信号,类似于SIGALRM,但是计算的是该进程占用的CPU时间。 | 终止 |
| 27 | SIGPROF | 类似于SIGALRM / SIGVTALRM,但包括该进程使用的CPU时间以及系统调用的时间。 | 终止 |
| 28 | SIGWINCH | 窗口大小改变时发出。 | 忽略 |
| 29 | SIGIO | 文件描述符准备就绪, 可以开始进行输入/输出操作。 | 终止 |
| 30 | SIGPWR | 启动失败时产生。 | 终止 |
| 31 | SIGUNUSED | 非法的系统调用。 | 终止并产生转储文件(core文件) |
- 该信号值列表仅对x86、PowerPC和ARM确认有效
- SIGKILL和SIGSTOP特殊,它们不能忽略、阻塞或捕获
实时信号与非实时信号
131非实时信号,不支持排队。若多次发送相同信号,只有一次会被处理。3464事实信号支持排队。
信号的处理
生成信号的事件分为三类:
- 程序错误:零作除数、非法内存访问等
- 外部事件:用户按键等
- 显式请求:进程通过
kill()发送信号 信号的处理动作: - 忽略:除SIGSTOP和SIGKILL外大部分信号都可忽略
- 捕获:当信号出现时调用函数,称其为信号处理函数
- 默认:让信号的默认动作起作用
实验
#include <stdio.h>
#include <unistd.h>
int main(void)
{
printf("\nthis is an singal test function\n\n");
while (1) {
printf("waiting for the SIGINT signal , please enter \"ctrl + c\"...\n");
sleep(1);
}
exit(0);
}
捕获信号
signal()
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
实验
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
/** 信号处理函数 */
void signal_handler(int sig)
{
printf("\nthis signal number is %d \n",sig);
if (sig == SIGINT) {
printf("I have get SIGINT!\n\n");
printf("The signal has been restored to the default processing mode!\n\n");
/** 恢复信号为默认情况 */
signal(SIGINT, SIG_DFL);
}
}
int main(void)
{
printf("\nthis is an singal test function\n\n");
/** 设置信号处理的回调函数 */
signal(SIGINT, signal_handler);
while (1) {
printf("waiting for the SIGINT signal , please enter \"ctrl + c\"...\n");
sleep(1);
}
exit(0);
}
sigaction()
函数原型
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
- signum:信号值
- oldact:返回原有的信号处理参数
- act:结构体如下
struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};
- sa_handler:信号处理函数
- sa_sigaction:扩展信号处理函数,不可与sa_handler共用
- sa_mask:信号掩码
- re_restore:已废弃
- sa_flags:一系列用于修改信号处理过程的行为的标志
- SA_NOCLDSTOP:如果signum是SIGCHLD,则在子进程停止或恢复时,不会传信号给调用sigaction()函数的进程。 即当它们接收到SIGSTOP、SIGTSTP、SIGTTIN或SIGTTOU(停止)中的一种时或接收到SIGCONT(恢复)时, 父进程不会收到通知。仅当为SIGCHLD建立处理程序时,此标志才有意义。
- SA_NOCLDWAIT:它表示父进程在它的子进程终止时不会收到SIGCHLD 信号, 这时子进程终止则不会成为僵尸进程。
- SA_NODEFER:不要阻止从其自身的信号处理程序中接收信号,使进程对信号的屏蔽无效, 即在信号处理函数执行期间仍能接收这个信号,仅当建立信号处理程序时,此标志才有意义。
- SA_RESETHAND:信号处理之后重新设置为默认的处理方式。
- SA_SIGINFO:指示使用sa_sigaction成员而不是使用sa_handler 成员作为信号处理函数。
siginfo_t的定义
siginfo_t {
int si_signo; /* 信号数值 */
int si_errno; /* 错误值 */
int si_code; /* 信号代码 */
int si_trapno; /*导致硬件生成信号的陷阱号,在大多数体系结构中未使用*/
pid_t si_pid; /* 发送信号的进程ID */
uid_t si_uid; /*发送信号的真实用户ID */
int si_status; /* 退出值或信号状态*/
clock_t si_utime; /*消耗的用户时间*/
clock_t si_stime; /*消耗的系统时间*/
sigval_t si_value; /*信号值*/
int si_int; /* POSIX.1b 信号*/
void *si_ptr;
int si_overrun; /*计时器溢出计数*/
int si_timerid; /* 计时器 ID */
void *si_addr; /*导致故障的内存位置 */
long si_band;
int si_fd; /* 文件描述符*/
short si_addr_lsb; /*地址的最低有效位 (从Linux 2.6.32开始存在) */
void *si_lower; /*地址冲突时的下限*/
void *si_upper; /*地址冲突时的上限 (从Linux 3.19开始存在) */
int si_pkey; /*导致的PTE上的保护密钥*/
void *si_call_addr; /*系统调用指令的地址*/
int si_syscall; /*尝试的系统调用次数*/
unsigned int si_arch; /* 尝试的系统调用的体系结构*/
}
实验
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
/** 信号处理函数 */
void signal_handler(int sig) //(1)
{
printf("\nthis signal number is %d \n",sig);
if (sig == SIGINT) {
printf("I have get SIGINT!\n\n");
printf("The signal is automatically restored to the default handler!\n\n");
/** 信号自动恢复为默认处理函数 */
}
}
int main(void)
{
struct sigaction act;
printf("this is sigaction function test demo!\n\n");
/** 设置信号处理的回调函数 */
act.sa_handler = signal_handler; //(2)
/* 清空屏蔽信号集 */
sigemptyset(&act.sa_mask); //(3)
/** 在处理完信号后恢复默认信号处理 */
act.sa_flags = SA_RESETHAND; //(4)
sigaction(SIGINT, &act, NULL); //(5)
while (1)
{
printf("waiting for the SIGINT signal , please enter \"ctrl + c\"...\n\n");
sleep(1);
}
exit(0);
}
发送信号
kill()和raise()
kill命令。
kill [信号或选项] PID(s)
kill函数。
int kill(pid_t pid, int sig);
- pid取值:
- pid > 1:将信号发送到PID进程
- pid = 0:将信号发送到所有和当前在同一个进程组的进程
- pid = -1:将信号发送多所有进程,除了pid=1(init)
- pid < -1:将信号发送到进程组号为-pid的进程组的每一个进程
- sig:信号值
- 返回值:
- 0:成功
- -1:失败
raise()只向自身发送信号。
int raise(int sig);
实验
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
int main(void)
{
pid_t pid;
int ret;
/* 创建一子进程 */
if ((pid = fork()) < 0) {
printf("Fork error\n");
exit(1);
}
if (pid == 0) {
/* 在子进程中使用 raise()函数发出 SIGSTOP 信号,使子进程暂停 */
printf("Child(pid : %d) is waiting for any signal\n\n", getpid());
/** 子进程停在这里 */
raise(SIGSTOP);
exit(0);
}
else {
/** 等待一下,等子进程先执行 */
sleep(1);
/* 在父进程中收集子进程发出的信号(不阻塞),并调用 kill()函数进行相应的操作 */
if ((waitpid(pid, NULL, WNOHANG)) == 0) {
/** 子进程还没退出,返回为0,就发送SIGKILL信号杀死子进程 */
if ((ret = kill(pid, SIGKILL)) == 0) {
printf("Parent kill %d\n\n",pid);
}
}
/** 一直阻塞直到子进程退出(杀死) */
waitpid(pid, NULL, 0);
exit(0);
}
}
alarm()
unsigned int alarm(unsigned int seconds);
当seconds时间计时完成后,向自身发送SIGALRM。如果在计时完成前再次设置,则会覆盖设置,并返回之前的剩余秒数。
实验
int main()
{
printf("\nthis is an alarm test function\n\n");
alarm(5); // SIGALRM的默认处理为执行exit(0)
sleep(20);
printf("end!\n");
return 0;
}
管道
概念
ps -ux | grep $USER
其中|就是管道运算符,它对标准输入/输出进行了重连接,在ps -ux和grep $USER间建立了数据管道。
分类
管道分为匿名管道和命名管道。
匿名管道PIPE
|就是匿名管道的一种。
- 没有名字。可应用
close()不能应用open() - 半双工通信
- 只能用于具有亲缘关系的进程间通信
- 基于字节流
- 依赖于文件系统,生命周期随进程结束而结束
- 不属于任何文件系统,但可以用
read()、write()读写
命名管道FIFO
- 有名字
- 存储于普通文件系统,可用
read()、write()读写 - 数据存储在内存(而不是磁盘)
- 全双工通信
- 先进先出
pipe()
pipe()用于创建一个匿名管道。
int pipe(int pipefd[2]);
pipefd[0] 指管道的读取端,pipefd[1]指向管道的写入端。
- 父进程 -> 子进程,则父进程关闭读取端,子进程关闭写入端
- 子进程 -> 父进程,则父进程关闭写入端,子进程关闭读取端
实验
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAX_DATA_LEN 256
#define DELAY_TIME 1
int main()
{
pid_t pid;
int pipe_fd[2]; //(1)
char buf[MAX_DATA_LEN];
const char data[] = "Pipe Test Program";
int real_read, real_write;
memset((void*)buf, 0, sizeof(buf));
/* 创建管道 */
if (pipe(pipe_fd) < 0) //(2)
{
printf("pipe create error\n");
exit(1);
}
/* 创建一子进程 */
if ((pid = fork()) == 0) //(3)
{
/* 子进程关闭写描述符,并通过使子进程暂停 3s 等待父进程已关闭相应的读描述符 */
close(pipe_fd[1]);
sleep(DELAY_TIME * 3);
/* 子进程读取管道内容 */ //(4)
if ((real_read = read(pipe_fd[0], buf, MAX_DATA_LEN)) > 0)
{
printf("%d bytes read from the pipe is '%s'\n", real_read, buf);
}
/* 关闭子进程读描述符 */
close(pipe_fd[0]); //(5)
exit(0);
}
else if (pid > 0)
{
/* 父进程关闭读描述符,并通过使父进程暂停 1s 等待子进程已关闭相应的写描述符 */
close(pipe_fd[0]); //(6)
sleep(DELAY_TIME);
if((real_write = write(pipe_fd[1], data, strlen(data))) != -1) //(7)
{
printf("Parent write %d bytes : '%s'\n", real_write, data);
}
/*关闭父进程写描述符*/
close(pipe_fd[1]); //(8)
/*收集子进程退出信息*/
waitpid(pid, NULL, 0); //(9)
exit(0);
}
}
mkfifo()
mkfifo()用于创建一个命名管道。
int mkfifo(const char * pathname,mode_t mode);
函数参数:
- pathname: 创建的文件路径
- mode:文件模式、权限
- O_RDONLY:只读管道
- O_WRONLY:只写管道
- O_RDWR:读写管道
- O_NONBLOCK:非阻塞
- O_CREAT:如果该文件不存在,那么就创建一个新的文件,并用第三个参数为其设置权限
- O_EXCL:如果使用 O_CREAT 时文件存在,那么可返回错误消息
函数返回值:
- 0:成功
- EACCESS:参数 filename 所指定的目录路径无可执行的权限
- EEXIST:参数 filename 所指定的文件已存在
- ENAMETOOLONG:参数 filename 的路径名称太长
- ENOENT:参数 filename 包含的目录不存在
- ENOSPC:文件系统的剩余空间不足
- ENOTDIR:参数 filename 路径中的目录存在但却非真正的目录
- EROFS:参数 filename 指定的文件存在于只读文件系统内
读操作时:
- 若管道是阻塞类型,则读进程将一直阻塞到有数据写入
- 若管道是非阻塞类型,则若FIFO内没有有数据,读函数将立刻返回 0
写操作时:
- 若管道是阻塞类型,则写操作将一直阻塞到数据可以被写入
- 若管道是非阻塞类型而不能写入全部数据,则写操作进行部分写入或者调用失败
实验
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
#include <string.h>
#define MYFIFO "myfifo" /* 命名管道文件名*/
#define MAX_BUFFER_SIZE PIPE_BUF /* 4096 定义在于 limits.h 中*/
void fifo_read(void)
{
char buff[MAX_BUFFER_SIZE];
int fd;
int nread;
printf("***************** read fifo ************************\n");
/* 判断命名管道是否已存在,若尚未创建,则以相应的权限创建*/
if (access(MYFIFO, F_OK) == -1) //(4)
{
if ((mkfifo(MYFIFO, 0666) < 0) && (errno != EEXIST)) //(5)
{
printf("Cannot create fifo file\n");
exit(1);
}
}
/* 以只读阻塞方式打开命名管道 */
fd = open(MYFIFO, O_RDONLY); //(6)
if (fd == -1)
{
printf("Open fifo file error\n");
exit(1);
}
memset(buff, 0, sizeof(buff));
if ((nread = read(fd, buff, MAX_BUFFER_SIZE)) > 0) // (7)
{
printf("Read '%s' from FIFO\n", buff);
}
printf("***************** close fifo ************************\n");
close(fd); //(8)
exit(0);
}
void fifo_write(void)
{
int fd;
char buff[] = "this is a fifo test demo";
int nwrite;
sleep(2); //等待子进程先运行 //(9)
/* 以只写阻塞方式打开 FIFO 管道 */
fd = open(MYFIFO, O_WRONLY | O_CREAT, 0644); //(10)
if (fd == -1)
{
printf("Open fifo file error\n");
exit(1);
}
printf("Write '%s' to FIFO\n", buff);
/*向管道中写入字符串*/
nwrite = write(fd, buff, sizeof(buff)); //(11)
if(wait(NULL)) //等待子进程退出
{
close(fd); //(12)
exit(0);
}
}
int main()
{
pid_t result;
/*调用 fork()函数*/
result = fork(); //(1)
/*通过 result 的值来判断 fork()函数的返回情况,首先进行出错处理*/
if(result == -1)
{
printf("Fork error\n");
}
else if (result == 0) /*返回值为 0 代表子进程*/
{
fifo_read(); //(2)
}
else /*返回值大于 0 代表父进程*/
{
fifo_write(); //(3)
}
return result;
}
系统规定:在一个以 O_WRONLY(即阻塞方式)打开的 FIFO 中,如果写入的数据长度小于等于 PIPE_BUF,那么要么写入全部字节,要么一个字节都不写入。这保证了多进程通信时数据不会发生交错。
消息队列
概念
消息队列和命名管道很像,但是:
- 消息队列可以独立于进程存在
- 消息队列发送的字节流可以具有格式、优先级
- 消息队列可以实现消息的随机查询,而不一定是先进先出
函数说明
msgget()获取消息队列
它的作用是创建或获取一个消息队列对象。 函数原型:
int msgget(key_t key, int msgflg);
函数参数:
- key:消息队列的关键字值,多个进程可以通过它访问同一个消息队列。 其中有个特殊值IPC_PRIVATE,它用于创建当前进程的私有消息队列
- msgflg:表示创建的消息队列的模式标志参数,主要有
IPC_CREAT,IPC_EXCL和权限modeIPC_CREAT:若内核中不存在关键字与key相等的信号量集合,则新建一个信号量集合; 如果存在,则返回此消息队列的标识符。IPC_CREAT | IPC_EXCL:若不存在键值与key相等的信号量集合,则新建一个信号量集合; 如果存在则报错mode:使用Linux文件的数字权限表示方式,如0600,0666等
返回值:
0:成功-1:出错,错误原因存于errno中EACCES:指定的消息队列已存在,但调用进程没有权限访问EEXIST:指定的消息队列已存在,msgflg中指定了IPC_CREAT和IPC_EXCLENOENT:指定的消息队列不存在,msgflg中没有指定IPC_CREAT标志ENOMEM:内存不足ENOSPC:已达到系统的限制
msgsnd()发送消息
函数原型:
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
函数参数:
- msqid:消息队列标识符
- msgp:发送给队列的消息。 msgp 可以是任何类型的结构体,但第一个字段必须为long类型, 即表明此发送消息的类型
- msgsz:要发送消息的大小,不包含消息类型占用的4个字节
- msgflg:
0:当消息队列满时,函数将阻塞IPC_NOWAIT:当消息队列满时,立即返回IPC_NOERROR:若发送消息大于 msgsz ,则把消息截断,且不通知发送进程
返回值:
0:成功-1:出错,错误原因存于errno中EAGAIN:参数 msgflg 设为IPC_NOWAIT,而消息队列已满EIDRM:标识符为 msqid 的消息队列已被删除EACCES:无权限写入消息队列EFAULT:参数 msgp 指向无效的内存地址EINTR:队列已满而处于等待情况下被信号中断EINVAL:无效的参数 msqid 、 msgsz 或参数消息类型小于0
msgrcv()接收消息
它在读取消息后会把消息从消息队列中删除。 函数原型:
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
函数参数:
- msqid:消息队列标识符
- msgp:存放消息的结构体
- msgsz:要接收消息的大小,不包含消息类型占用的4个字节
- msgtyp:有多个可选的值:如果为0则表示接收第一个消息,如果大于0则表示接收类型等于 msgtyp 的第一个消息, 而如果小于0则表示接收类型等于或者小于 msgtyp 绝对值的第一个消息
- msgflg:
0: 阻塞式接收消息IPC_NOWAIT:当没有可接收的消息,则立即返回,错误码为ENOMSGIPC_EXCEPT:与 msgtype 配合使用,返回队列中第一个类型不为 msgtype 的消息IPC_NOERROR:若接收消息大于 msgsz ,则把消息截断
返回值
0:成功-1:出错,错误原因存于errno中E2BIG:消息数据长度大于 msgsz 而 msgflag 没有设置IPC_NOERROREIDRM:标识符为 msqid 的消息队列已被删除EACCESS:无权限读取该消息队列EFAULT:参数 msgp 指向无效的内存地址EINTR:等待读取队列内的消息情况下被信号中断
msgctl操作消息
它用来设置或者获取消息队列的相关属性。 函数原型:
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
函数参数
- msqid:消息队列标识符
- cmd:操作命令
IPC_STAT:获取该消息队列的信息IPC_SET:设置消息队列的属性,要设置的属性需先存储在结构体 msqid_ds 类型的buf中, 可设置的属性:msg_perm.uid、msg_perm.gid、msg_perm.mode以及msg_qbytesIPC_RMID:立即删除该消息队列,并且唤醒所有阻塞在该消息队列上的进程IPC_INFO:获得关于当前系统中消息队列的限制值信息MSG_INFO:获得关于当前系统中消息队列的相关资源消耗信息MSG_STAT:同IPC_STAT,但可以获得系统中所有消息队列的信息
- buf:结构体缓冲区
返回值:
0:成功-1:出错,错误原因存于errno中EACCESS:参数 cmd 为IPC_STAT,无权限读取EFAULT:参数 buf 指向无效的内存地址。EIDRM:标识符为 msqid 的消息队列已被删除。EINVAL:无效的参数 cmd 或 msqidEPERM:参数 cmd 为IPC_SET或IPC_RMID,无权限执行
实验
发送进程
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#define BUFFER_SIZE 512
struct message
{
long msg_type;
char msg_text[BUFFER_SIZE];
};
int main()
{
int qid;
struct message msg;
/*创建消息队列*/ if ((qid = msgget((key_t)1234, IPC_CREAT|0666)) == -1)
{
perror("msgget\n");
exit(1);
}
printf("Open queue %d\n",qid);
while(1)
{
printf("Enter some message to the queue:");
if ((fgets(msg.msg_text, BUFFER_SIZE, stdin)) == NULL)
{
printf("\nGet message end.\n");
exit(1);
}
msg.msg_type = getpid();
/*添加消息到消息队列*/ if ((msgsnd(qid, &msg, strlen(msg.msg_text), 0)) < 0)
{
perror("\nSend message error.\n");
exit(1);
}
else
{
printf("Send message.\n");
}
if (strncmp(msg.msg_text, "quit", 4) == 0)
{
printf("\nQuit get message.\n");
break;
}
}
exit(0);
}
接收进程
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#define BUFFER_SIZE 512
struct message
{
long msg_type;
char msg_text[BUFFER_SIZE];
};
int main()
{
int qid;
struct message msg;
/*创建消息队列*/
if ((qid = msgget((key_t)1234, IPC_CREAT|0666)) == -1) {
perror("msgget");
exit(1);
}
printf("Open queue %d\n", qid);
do
{
/*读取消息队列*/
memset(msg.msg_text, 0, BUFFER_SIZE);
if (msgrcv(qid, (void*)&msg, BUFFER_SIZE, 0, 0) < 0) {
perror("msgrcv");
exit(1);
}
printf("The message from process %ld : %s", msg.msg_type, msg.msg_text);
} while(strncmp(msg.msg_text, "quit", 4));
/*从系统内核中删除消息队列 */
if ((msgctl(qid, IPC_RMID, NULL)) < 0) {
perror("msgctl");
exit(1);
}
else
{
printf("Delete msg qid: %d.\n", qid);
}
exit(0);
}
System-V IPC 信号量
概念
信号量不以传送数据为主要目的,它主要用来保护共享资源。
- 临界区:访问共享资源的代码段,在同一时刻只允许一个进程或线程进入执行。
- P操作:申请资源,使信号量的值减 1;若信号量值不足(小于 0 或不可用),进程阻塞等待,直到资源可用。常用于进入临界区前的加锁操作。
- V操作:释放资源,使信号量的值加 1;若有进程因等待该信号量而阻塞,则唤醒其中一个或多个进程继续执行。常用于离开临界区后的解锁操作。
PV操作都为原子操作。
函数说明
semget()获取信号量
它用于创建或获取一个已有的信号量。 函数原型:
int semget(key_t key, int nsems, int semflg);
函数参数:
- key:标识符。可以使用
IPC_PRIVATE创建一个没有key的信号量。 - nsems:表示可用的信号量数目。
- semflg:semflg 参数用来指定标志位,与消息队列中的类似。
IPC_CREAT:若内核中不存在关键字与key相等的信号量集合,则新建一个信号量集合; 如果存在,则返回此消息队列的标识符。IPC_CREAT | IPC_EXCL:若不存在键值与key相等的信号量集合,则新建一个信号量集合; 如果存在则报错
创建信号量时,还受到以下参数影响:
watermeko@DESKTOP-2ER8RB2 ~> ipcs -l
------ Semaphore Limits --------
max number of arrays = 32000
max semaphores per array = 32000
max semaphores system wide = 1024000000
max ops per semop call = 500
semaphore max value = 32767
semop()操作信号量
它用来对信号量进行PV操作。 函数原型:
int semop(int semid, struct sembuf *sops, size_t nsops);
函数参数:
- semid:标识符
- sops:信号量操作数组
- nsops:sops 数组的数量
信号量操作结构体:
struct sembuf {
unsigned short int sem_num;
short int sem_op;
short int sem_flg;
};
结构体参数:
- sem_num:标识信号量中的第几个信号量,取值:
0 ~ nsems-1。 - sem_op:进行的操作类型:
sem_op > 0:执行V操作,释放资源数为 sem_op。sem_op < 0:执行P操作, 申请资源数为-sem_op 。若信号量值小于 -sem_op ,则相应信号量的等待进程数量就加1,调用进程被阻塞。sem_op = 0:阻塞等待,直至信号量值变为 0。
- sem_flg:
0:正常操作(阻塞式)。IPC_NOWAIT:立即返回-1,并将errno设定为EAGAIN。SEM_UNDO:进程退出时将自动还原它对信号量的操作。
semctl()控制信号量
它用来设置或者获取信号量的相关属性。 函数原型:
int semctl(int semid, int semnum, int cmd, ...);
- semid:信号量标识符。
- semnum:标识信号量中的第几个信号量,取值:
0 ~ nsems-1。 - cmd:
IPC_STAT:获取此信号量集合的描述结构体,存放在第四个参数的buf中。IPC_SET:通过第四个参数的buf来设定信号量集相关联的semid_ds中信号量集合权限为sem_perm中的uid,gid,mode。IPC_RMID:删除该信号量集合。GETVAL:返回第 semnum 个信号量的值。SETVAL:设置第 semnum 个信号量的值,该值由第四个参数中的val指定。GETPID:返回第 semnum 个信号量的 sempid ,最后一个操作的pid。GETNCNT:返回第 semnum 个信号量的 semncnt,即等待该信号量值增加的进程数。GETZCNT:返回第 semnum 个信号量的 semzcnt。等待semval变为0的线程数。GETALL:取信号量集合中所有信号量的值,将结果存放到的array所指向的数组。SETALL:按arg.array所指向的数组中的值,设置集合中所有信号量的值。
- 第四个参数可选:
union semun {
实验
封装后的信号量操作函数:
#include <sys/sem.h>
#include <sys/ipc.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/shm.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include "sem.h"
int init_sem(int sem_id, int init_value)
{
union semun sem_union; sem_union.val = init_value;
if (semctl(sem_id, 0, SETVAL, sem_union) == -1) {
perror("Initialize semaphore");
return -1;
}
return 0;
}
int del_sem(int sem_id)
{
union semun sem_union;
if (semctl(sem_id, 0, IPC_RMID, sem_union) == -1) {
perror("Delete semaphore");
return -1;
}
}
int sem_p(int sem_id)
{
struct sembuf sops;
sops.sem_num = 0;
sops.sem_op = -1;
sops.sem_flg = SEM_UNDO;
if (semop(sem_id, &sops, 1) == -1) {
perror("P operation");
return -1;
}
return 0;
}
int sem_v(int sem_id)
{
struct sembuf sops;
sops.sem_num = 0;
sops.sem_op = 1;
sops.sem_flg = SEM_UNDO;
if (semop(sem_id, &sops, 1) == -1) {
perror("V operation");
return -1;
}
return 0;
}
主程序:
#include <sys/types.h>
#include <sys/shm.h>
#include <sys/sem.h>
#include <sys/ipc.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include "sem.h"
#define DELAY_TIME 3
int main(void)
{
pid_t result;
int sem_id;
sem_id = semget((key_t)6666, 1, 0666 | IPC_CREAT);
init_sem(sem_id, 0);
result = fork();
if(result == -1)
{
perror("Fork\n");
}
else if (result == 0){
printf("Child process will wait for some seconds...\n");
sleep(DELAY_TIME);
printf("The returned value is %d in the child process(PID = %d)\n",result, getpid());
sem_v(sem_id);
}
else{
sem_p(sem_id);
printf("The returned value is %d in the father process(PID = %d)\n",result, getpid());
sem_v(sem_id);
del_sem(sem_id);
}
exit(0);
}
共享内存
概念
共享内存是效率最高的IPC通信机制。由于其本身没有同步机制,因此需要信号量、互斥锁等机制协助工作。
函数说明
shmget()创建共享内存
用来创建或获取共享内存对象。 函数原型:
int shmget(key_t key, size_t size, int shmflg);
参数说明:
- key
0:若 shmflg 设置了IPC_PRIVATE则创建新的共享内存IPC_PRIVATE:创建新的共享内存int number > 0:视参数shmflg来确定操作
- size:共享内存的大小,以字节为单位,但会向上取整到页
- shmflg
IPC_CREAT:创建或返回共享内存标识符IPC_EXCL:创建,若存在则报错SHM_HUGETLB:使用"大页面"来分配共享内存SHM_NORESERVE:不在交换分区中为这块共享内存保留空间
shmat()映射内存
将物理共享内存映射到进程的虚拟内存中。 函数原型:
void *shmat(int shmid, const void *shmaddr, int shmflg);
函数参数:
- shmaddr:
NULL:自动选择虚拟内存地址非NULL:根据 shmadder 选择虚拟内存地址
- shmflg
SHM_RDONLY:以只读方式映射共享内存SHM_REMAP:重新映射,此时 shmaddr 不能为NULLNULLSHM:自动选择比 shmaddr 小的最大页对齐地址
shmdt()解映射内存
与shmat()作用相反。
函数原型:
int shmdt(const void *shmaddr);
函数参数:
- shmaddr:映射的共享内存的起始地址
shmctl()设置共享内存属性
函数原型:
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
函数参数:
- cmd
IPC_STAT:获取属性信息,放置到buf中IPC_SET:设置属性信息为buf指向的内容IPC_RMID:删除该共享内存IPC_INFO:获得关于共享内存的系统限制值信息SHM_INFO:获得系统为共享内存消耗的资源信息SHM_STAT:与IPC_STAT具有相同的功能,但shmid为该SHM在内核中记录所有SHM信息的数组的下标, 因此通过迭代所有的下标可以获得系统中所有SHM的相关信息SHM_LOCK:禁止系统将该SHM交换至swap分区SHM_UNLOCK:允许系统将该SHM交换至swap分区
- buf:共享内存属性信息结构体指针
struct shmid_ds {
struct ipc_perm shm_perm; /* 所有权和权限 */
size_t shm_segsz; /* 共享内存尺寸(字节) */
time_t shm_atime; /* 最后一次映射时间 */
time_t shm_dtime; /* 最后一个解除映射时间 */
time_t shm_ctime; /* 最后一次状态修改时间 */
pid_t shm_cpid; /* 创建者PID */
pid_t shm_lpid; /* 后一次映射或解除映射者PID */
shmatt_t shm_nattch; /* 映射该SHM的进程个数 */
...
};
struct ipc_perm {
key_t __key; /* 该共享内存的键值key */
uid_t uid; /* 所有者的有效UID */
gid_t gid; /* 所有者的有效GID */
uid_t cuid; /* 创建者的有效UID */
gid_t cgid; /* 创建者的有效GID */
unsigned short mode; /* 读写权限 + SHM_DEST + SHM_LOCKED 标记 */
unsigned short __seq; /* 序列号 */
};
实验
首先创建system-V信号量用于同步,然后分别实现共享内存写进程和共享内存读进程。
写进程
#include <sys/types.h>
#include <sys/shm.h>
#include <sys/sem.h>
#include <sys/ipc.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include "sem.h"
int main()
{
int running = 1;
void *shm = NULL;
struct shared_use_st *shared = NULL;
char buffer[BUFSIZE + 1];
int shmid;
int semid;
shmid = shmget((key_t)1234, 4096, 0644 | IPC_CREAT);
if(shmid == -1)
{
fprintf(stderr, "shmget failed\n");
exit(EXIT_FAILURE);
}
shm = shmat(shmid, (void*)0, 0);
if(shm == (void*)-1)
{
fprintf(stderr, "shmat failed\n");
exit(EXIT_FAILURE);
}
printf("Memory attached at %p\n", shm);
semid = semget((key_t)6666, 1, 0666|IPC_CREAT);
if(semid == -1)
{
printf("sem open fail\n");
exit(EXIT_FAILURE);
}
while(running)
{
printf("Enter some text: ");
fgets(buffer, BUFSIZE, stdin);
strncpy(shm, buffer, 4096);
sem_v(semid);
if(strncmp(buffer, "end", 3) == 0)
running = 0;
}
if(shmdt(shm) == -1) {
fprintf(stderr, "shmdt failed\n");
exit(EXIT_FAILURE);
}
sleep(2);
exit(EXIT_SUCCESS);
}
读进程
#include <sys/types.h>
#include <sys/shm.h>
#include <sys/sem.h>
#include <sys/ipc.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include "sem.h"
int main(void)
{
int running = 1;
char *shm = NULL;
int shmid;
int semid;
shmid = shmget((key_t)1234, 4096, 0666 | IPC_CREAT);
if(shmid == -1)
{
fprintf(stderr, "shmget failed\n");
exit(EXIT_FAILURE);
}
shm = shmat(shmid, 0, 0);
if(shm == (void*)-1)
{
fprintf(stderr, "shmat failed\n");
exit(EXIT_FAILURE);
}
printf("\nMemory attached at %p\n", shm);
semid = semget((key_t)6666, 1, 0666|IPC_CREAT);
if(semid == -1)
{
printf("sem open fail\n");
exit(EXIT_FAILURE);
}
init_sem(semid, 0);
while(running)
{
if(sem_p(semid) == 0)
{
printf("You wrote: %s", shm);
sleep(rand() % 3);
if(strncmp(shm, "end", 3) == 0)
running = 0;
}
}
del_sem(semid);
if(shmdt(shm) == -1)
{
fprintf(stderr, "shmdt failed\n");
exit(EXIT_FAILURE);
}
if(shmctl(shmid, IPC_RMID, 0) == -1)
{
fprintf(stderr, "shmctl(IPC_RMID) failed\n");
exit(EXIT_FAILURE);
}
exit(EXIT_SUCCESS);
}
线程
概念
进程是资源管理的最小单位,每个进程都具有独立的上下文。因此切换进程带来的开销很大。 线程是程序执行的最小单位,也称为轻量级进程。同一进程内的多个线程共享代码段、数据段、打开的文件等资源,但各自拥有独立的栈、寄存器和执行流。由于线程切换不需要切换整个进程资源,因此开销比进程切换小,适合实现并发任务。
POSIX
POSIX(Portable Operating System Interface) 是 IEEE 制定的一组操作系统接口标准,定义了操作系统应为应用程序提供的 API、shell 工具和系统行为的统一规范。它主要目的是让为 POSIX 兼容系统编写的程序能在不同 Unix/Linux 系统之间方便地移植,比如 pthread(POSIX 线程)就是其中关于多线程接口的标准。
pthread是由POSIX提出的线程库。它并不默认包含在Linux库中,因此链接时需要加上-lpthread。
线程创建
函数原型:
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
函数参数:
- thread:指向线程标识符的指针。
- attr:设置线程属性。
- start_routine:start_routine是一个函数指针,指向要运行的线程入口,即线程运行时要执行的函数代码。
- arg:运行线程时传入的参数。
函数报错在返回值中,并不设定errno。
线程属性
typedef struct
{
int detachstate; //线程的分离状态
int schedpolicy; //线程调度策略
struct sched_param schedparam; //线程的调度参数
int inheritsched; //线程的继承性
int scope; //线程的作用域
size_t guardsize; //线程栈末尾的警戒缓冲区大小
int stackaddr_set; //线程的栈设置
void* stackaddr; //线程栈的位置
size_t stacksize; //线程栈的大小
}pthread_attr_t;
线程相关的属性非常多,并且不能直接设置。必须通过相关API函数设定。
分离状态
线程默认是可连接(joinable)状态,结束后仍保留退出信息和部分资源,必须由其他线程调用 pthread_join 回收。若设置为分离(detached)状态,线程结束时资源会自动释放,不能再被 pthread_join 等待。可通过 pthread_attr_setdetachstate 在线程创建前设置,或创建后调用 pthread_detach。
调度策略
Linux 线程的调度策略决定了线程获得 CPU 的方式和优先级规则。可以通过 pthread_setschedparam 设置线程调度策略和优先级。
- 分时调度策略:
SCHED_OTHER。这是线程属性的默认值,另外两种调度方式只能用于以超级用户权限运行的进程, 因为它们都具备实时调度的功能,但在行为上略有区别。 - 实时调度策略:先进先出方式调度(
SCHED_FIFO)。基于队列的调度程序,对于每个优先级都会使用不同的队列, 先进入队列的线程能优先得到运行,线程会一直占用CPU,直到有更高优先级任务到达或自己主动放弃CPU使用权。 - 实时调度策略:时间片轮转方式调度(
SCHED_RR)。与 FIFO相似,不同的是前者的每个线程都有一个执行时间配额, 当采用SCHED_RR策略的线程的时间片用完,系统将重新分配时间片, 并将该线程置于就绪队列尾,并且切换线程,放在队列尾保证了所有具有相同优先级的RR线程的调度公平。
优先级
优先级数字越小,优先级越高(在FreeRTOS中是数字越大优先级越高)。调度策略为SCHED_OTHER时,静态优先级必须为0。这些线程会按照动态优先级被调度。动态优先级会随着运行时间的增加而降低,以确保所有线程获得公平的CPU时间。
线程栈
线程栈是每个线程独立拥有的内存区域,用于存储局部变量、函数调用参数和返回地址等。当线程执行函数时,栈帧会依次压入栈中,函数返回时弹出。栈空间通常较小(如1MB),且是线程私有的,因此不需要加锁保护。
线程退出
线程创建后,系统开始运行相关的线程函数,当这个函数运行完成后,线程会自动退出。但也有显式退出的函数pthread_exit()。在线程中调用exit()是错误的,因为它会退出进程。
如果一个线程想等待另一个线程退出,可以使用pthread_join。
实验
除非特别需要,否则几乎无需修改线程属性。
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
/*要执行的线程*/
void *test_thread(void *arg)
{
int num = (unsigned long long)arg; /** sizeof(void*) == 8 and sizeof(int) == 4 (64 bits) */
printf("This is test thread, arg is %d\n", num);
sleep(5);
/*退出线程*/
pthread_exit(NULL);
}
int main(void)
{
pthread_t thread;
void *thread_return;
int arg = 520;
int res;
printf("start create thread\n");
/*创建线程,线程为test_thread函数*/
res = pthread_create(&thread, NULL, test_thread, (void*)(unsigned long long)(arg));
if(res != 0)
{
printf("create thread fail\n");
exit(res);
}
printf("create threads success\n");
printf("waiting for threads to finish...\n");
/*等待线程终止*/
res = pthread_join(thread, &thread_return); if(res != 0)
{
printf("thread exit fail\n");
exit(res);
}
printf("thread exit ok\n");
return 0;
}
POSIX 信号量
概念
临界区是指执行数据更新的代码需要独占式地执行。临界资源是在同一个时刻只允许有限个(通常只有一个)进程/线程可以访问(读)或修改(写)的资源, 通常包括硬件资源和软件资源。 与system-V信号量的区别:POSIX信号量更加轻量级,提供命名信号量(基于文件)和无名信号量(基于内存),适用于线程和进程同步;而System V信号量基于内核的标识符,功能更强大但使用更复杂,通常只能用于进程间同步。此外,POSIX信号量的API更为简洁直观。 无名信号量一般用于进程/线程间同步或互斥,而有名信号量一般用于进程间同步或互斥。 有名信号量和无名信号量的差异在于创建和销毁的形式上,但是其他工作一样,无名信号量则直接保存在内存中, 而有名信号量则要求创建一个文件。
无名信号量
一般用于子进程/线程间同步或互斥,它保存在内存中。
int sem_init(sem_t *sem, int pshared, unsigned int value);
int sem_destroy(sem_t *sem);
int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);
int sem_post(sem_t *sem);
pshared 用于指定信号量在进程还是线程间共享。
线程间同步
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
static int glob = 0;
static sem_t sem;
static void *threadFunc(void *arg) {
int loops = *((int *) arg);
int loc, j;
for (j = 0; j < loops; j++) {
// P操作: 申请资源,如果信号量值为0则阻塞
if (sem_wait(&sem) == -1) {
perror("sem_wait");
return NULL;
}
// --- 临界区开始 ---
loc = glob;
loc++;
glob = loc;
// --- 临界区结束 ---
// V操作: 释放资源,并唤醒等待的线程
if (sem_post(&sem) == -1) {
perror("sem_post");
return NULL;
}
}
return NULL;
}
int main(int argc, char *argv[]) {
pthread_t t1, t2;
int loops = 10000000; // 循环次数
// 初始化无名信号量
// 参数2 (pshared): 0 表示用于线程间同步
// 参数3 (value): 1,初始资源数为1,相当于互斥锁
if (sem_init(&sem, 0, 1) == -1) {
perror("sem_init");
return 1;
}
pthread_create(&t1, NULL, threadFunc, &loops);
pthread_create(&t2, NULL, threadFunc, &loops);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
printf("glob = %d\n", glob); // 结果总是20000000
sem_destroy(&sem); // 销毁信号量
return 0;
}
子进程间同步
需要配合共享内存使用。
// 编译时通常需要链接 rt 库: gcc your_program.c -lrt
#include <stdio.h>
#include <stdlib.h>
#include <semaphore.h>
#include <sys/shm.h>
#include <sys/wait.h>
#include <unistd.h>
#define MAX_PROC 5
int main() {
int shm_id;
sem_t *sem_p;
pid_t pid;
// 1. 创建共享内存段
shm_id = shmget(IPC_PRIVATE, sizeof(sem_t), IPC_CREAT | 0666);
if (shm_id == -1) {
perror("shmget failed");
exit(1);
}
// 2. 将共享内存附加到本进程地址空间
sem_p = (sem_t *)shmat(shm_id, NULL, 0);
if (sem_p == (void *)-1) {
perror("shmat failed");
exit(1);
}
// 3. 在共享内存上初始化无名信号量
// 参数2 (pshared): 1,表示该信号量将在进程间共享
// 参数3 (value): 1,初始资源数为1
if (sem_init(sem_p, 1, 1) == -1) {
perror("sem_init failed");
exit(1);
}
for (int i = 0; i < MAX_PROC; i++) {
pid = fork();
if (pid == 0) { // 子进程
// 子进程同样通过附加的地址 sem_p 来访问信号量
printf("子进程 %d 正在等待进入临界区...\n", getpid());
sem_wait(sem_p); // 申请进入
printf("子进程 %d 已进入临界区,工作中...\n", getpid());
sleep(1); // 模拟工作
printf("子进程 %d 退出临界区\n", getpid());
sem_post(sem_p); // 释放资源
exit(0);
} else if (pid < 0) {
perror("fork failed");
}
}
// 父进程等待所有子进程结束
for (int i = 0; i < MAX_PROC; i++) {
wait(NULL);
}
// 清理工作
sem_destroy(sem_p);
shmdt(sem_p);
shmctl(shm_id, IPC_RMID, NULL);
return 0;
}
运行效果:
watermeko@DESKTOP-2ER8RB2 ~/D/linux_learn> ./test
子进程 184410 正在等待进入临界区...
子进程 184410 已进入临界区,工作中...
子进程 184411 正在等待进入临界区...
子进程 184412 正在等待进入临界区...
子进程 184413 正在等待进入临界区...
子进程 184414 正在等待进入临界区...
子进程 184410 退出临界区
子进程 184411 已进入临界区,工作中...
子进程 184411 退出临界区
子进程 184412 已进入临界区,工作中...
子进程 184412 退出临界区
子进程 184413 已进入临界区,工作中...
子进程 184413 退出临界区
子进程 184414 已进入临界区,工作中...
子进程 184414 退出临界区
有名信号量
一般用于进程间同步或互斥,它保存在文件中。
sem_t *sem_open(const char *name, int oflag, mode_t mode, unsigned int value);
int sem_wait(sem_t *sem); // P操作
int sem_trywait(sem_t *sem); // 非阻塞wait
int sem_post(sem_t *sem);
int sem_close(sem_t *sem);
int sem_unlink(const char *name);
进程同步
进程1:
#include <fcntl.h>
#include <semaphore.h>
#include <stdio.h>
#include <unistd.h>
int main() {
// 创建或打开一个名为 "/my_sem" 的信号量,初始值为 0
// 位于/dev/shm/sem.my_sem
sem_t *sem = sem_open("/my_sem", O_CREAT, 0644, 0);
if (sem == SEM_FAILED) {
perror("sem_open failed");
return 1;
}
printf("进程1: 等待进程2的信号...\n");
sem_wait(sem); // 阻塞,等待信号量值变为正
printf("进程1: 收到信号,继续执行!\n");
sem_close(sem);
return 0;
}
进程2
#include <fcntl.h>
#include <semaphore.h>
#include <stdio.h>
#include <unistd.h>
int main() {
// 打开已存在的 "/my_sem" 信号量
sem_t *sem = sem_open("/my_sem", 0);
if (sem == SEM_FAILED) {
perror("sem_open failed");
return 1;
}
printf("进程2: 正在执行准备工作...\n");
sleep(2); // 模拟工作
printf("进程2: 准备工作完成,发送信号给进程1\n");
sem_post(sem); // 增加信号量的值,唤醒在等待的进程1
sem_close(sem);
return 0;
}
POSIX 互斥锁
概念
互斥锁与二值信号量的区别:互斥锁具有所有权机制,只能由持有锁的任务释放,适合保护临界区;二值信号量无所有权概念,任何任务均可释放,常用于同步或事件通知。此外,互斥锁支持优先级继承,可防止优先级反转,而二值信号量需手动处理此问题。因此互斥锁比二值信号量更适合用于保护临界资源。
初始化互斥锁
静态初始化和动态初始化的区别:静态初始化使用宏 PTHREAD_MUTEX_INITIALIZER 在编译时完成,适用于全局或静态变量,无需调用函数,也不检查错误。动态初始化则通过 pthread_mutex_init 函数在运行时进行,可以设置自定义属性,并返回错误码,适用于堆上或局部变量及需要指定互斥锁类型的场景。
静态初始化
pthread_mutex_t fastmutex = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t recmutex = PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP;
pthread_mutex_t errchkmutex = PTHREAD_ERRORCHECK_MUTEX_INITIALIZER_NP;
PTHREAD_MUTEX_INITIALIZER:默认互斥锁,即快速互斥锁。PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP:递归互斥锁。互斥锁被线程1持有时,线程2尝试获取互斥锁, 将无法获取成功,并且阻塞等待,而如果是线程1尝试再次获取互斥锁时,将获取成功,并且持有互斥锁的次数加1。PTHREAD_ERRORCHECK_MUTEX_INITIALIZER_NP:检错互斥锁。快速互斥锁的非阻塞版本。
动态初始化
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr);
获取/释放互斥锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex); // unlock的非阻塞版本
int pthread_mutex_unlock(pthread_mutex_t *mutex);
销毁互斥锁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
示例
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#define THREAD_NUMBER 3 /* 线程数 */
pthread_mutex_t mutex;
void *thread_func(void *arg)
{
int num = (unsigned long long)arg; /** sizeof(void*) == 8 and sizeof(int) == 4 (64 bits) */
int sleep_time = 0;
int res;
/* 互斥锁上锁 */
res = pthread_mutex_lock(&mutex);
if (res)
{ /*获取失败*/
printf("Thread %d lock failed\n", num);
/* 互斥锁解锁 */
pthread_mutex_unlock(&mutex);
pthread_exit(NULL);
}
printf("Thread %d is hold mutex\n", num);
/*睡眠一定时间*/
sleep(2);
printf("Thread %d freed mutex\n\n", num);
/* 互斥锁解锁 */
pthread_mutex_unlock(&mutex);
pthread_exit(NULL);
}
int main(void)
{
pthread_t thread[THREAD_NUMBER];
int num = 0, res;
/* 互斥锁初始化 */
pthread_mutex_init(&mutex, NULL);
for (num = 0; num < THREAD_NUMBER; num++)
{
/*创建线程*/
res = pthread_create(&thread[num], NULL, thread_func, (void*)(unsigned long long)num);
if (res != 0)
{
printf("Create thread %d failed\n", num);
exit(res);
}
}
for (num = 0; num < THREAD_NUMBER; num++)
{
/*等待线程结束*/
pthread_join(thread[num], NULL);
}
/*销毁互斥锁*/
pthread_mutex_destroy(&mutex);
return 0;
}
网络编程
简介
TCP/IP是一个协议族,包含众多协议。如HTTP、FTP、MQTT。
网络协议的分层模型
| 层级 | 主要协议 |
|---|---|
| 应用层 | HTTP、FTP、SMTP、DNS、MQTT |
| 传输层 | TCP、UDP |
| 网络层 | IP、ICMP、IGMP |
| 网络接口层 | 以太网、ARP、RARP |
协议层报文间的封装与拆封
当设备的网卡接收到某个数据包后,它会将其放置在网卡的接收缓存中,并告知TCP/IP内核。然后TCP/IP内核就开始工作了,它会将数据包从接收缓存中取出,并逐层解析数据包中的协议首部信息,并最终将数据交给某个应用程序。数据的接收过程与发送过程正好相反,可以概括为TCP/IP的各层协议对数据进行解析的过程。
IP协议
IP协议负责将数据报从源主机发送到目标主机,通过IP地址作为唯一识别码。它无连接、无错误检查与恢复机制。
IP地址
| 类别 | 地址范围 | 默认子网掩码 |
|---|---|---|
| A类 | 1.0.0.0 ~ 126.255.255.255 | 255.0.0.0 |
| B类 | 128.0.0.0 ~ 191.255.255.255 | 255.255.0.0 |
| C类 | 192.0.0.0 ~ 223.255.255.255 | 255.255.255.0 |
特殊地址
受限广播地址
255.255.255.255此地址指定本网段中的所有主机。
直接广播地址
它的网络号与主机号都为1。该地址可以向网络内的所有主机发送数据。
- A类地址的广播地址为:
XXX.255.255.255 - B类地址的广播地址为:
XXX. XXX.255.255 - C类地址的广播地址为:
XXX. XXX. XXX.255它只能作为目的地址。
多播地址
用于一对多的通信。只能作为目的地址。
环回地址
127.XXX.XXX.XXX表示自身。
本网络本主机
0.0.0.0只能作为源地址。用于在设备启动时,向DHCP服务器发送请求以获得IP地址。
UDP协议
UDP(用户数据报协议)是一种无连接的传输层协议,它提供不可靠但高效的数据传输服务。特点:
- 无连接、不可靠
- 丢包不重发
- 支持N对N通信
- 速度快、无握手机制
TCP协议
- UDP以报文为单位传输,各报文独立无序抵达,应用层需自行处理重装问题。
- TCP以数据流形式传输,通过为每个字节独立编号(双方方向独立)以确保数据的有序组装与确认,仅当数据完整接收后才交付应用层。
TCP特性
- 连接机制:TCP是面向连接的协议,通信前必须先通过三次握手建立连接,确保双方收发能力正常。通信结束后,通过四次挥手释放连接,保证数据完整传输。
- 确认与重传:接收方收到数据后需返回确认(ACK),若发送方在超时时间内未收到ACK,则重传该数据段。同时,TCP使用序列号和确认号实现数据有序与完整性,并引入快速重传机制,在收到重复ACK时立即重传,避免超时等待。
- 缓冲机制:TCP为每个连接维护发送缓冲区和接收缓冲区。发送方将数据暂存于发送缓冲区,等待网络传输;接收方将收到的数据放入接收缓冲区,供应用层按序读取。
- 流量控制:接收方通过TCP报文头中的窗口字段告知对方自己的可用接收缓冲区大小,发送方据此调整发送速率,避免因发送过快导致接收方缓冲区溢出。
- 全双工通信
- 差错控制:TCP使用校验和字段检测数据在传输过程中是否出现位错误,接收方发现校验和不匹配时会丢弃该报文段,不发送确认,发送方因超时而重传。
- 拥塞控制:TCP通过拥塞控制算法(如慢启动、拥塞避免、快速重传和快速恢复)动态调整发送窗口大小,防止网络过载。当检测到丢包(超时或重复ACK)时,发送方会减小拥塞窗口以缓解拥塞;在拥塞减轻后逐步增大窗口,平衡网络利用率与稳定性。
端口号
| 端口号 | 协议 | 说明 |
|---|---|---|
| 20/21 | FTP | 文件传输协议,使得主机间可以共享文件。 |
| 22 | SSH | 安全外壳协议。 |
| 23 | Telnet | 终端远程登录,它为用户提供了在本地计算机上完成远程主机工作的能力。 |
| 25 | SMTP | 简单邮件传输协议,它帮助每台计算机在发送或中转信件时找到下一个目的地。 |
| 69 | TFTP | 普通文件传输协议。 |
| 80 | HTTP | 超文本传输协议,通过使用网页浏览器、网络爬虫或者其它的工具,客户端发起一个HTTP请求到服务器上指定端口(默认端口为80),应答的服务器上存储着一些资源,比如HTML文件和图像,那么就会返回这些数据到客户端。 |
| 110 | POP3 | 邮局协议版本3,本协议主要用于支持使用客户端远程管理在服务器上的电子邮件。 |
TCP报文段
- URG:首部中的紧急指针字段标志,如果是1表示紧急指针字段有效。
- 紧急指针:紧急指针是一个16位的正偏移量,与TCP报文段中的序号字段相加,得到的值指向紧急数据最后一个字节的序号。当URG标志为1时,接收方应优先处理该紧急数据,而无需按正常顺序等待。
- ACK:首部中的确认序号字段标志,如果是1表示确认序号字段有效。
- 确认序号:表示期望收到对方下一个报文段数据部分的第一个字节的序号,用于确认已成功接收的数据,通常为上次接收到的序号加1。
- PSH:该字段置1表示接收方应该尽快将这个报文段交给应用层。
- RST:重新建立TCP连接。
- SYN:用同步序号发起连接。
- FIN:中止连接。
TCP建立连接
第一步:客户端发送 SYN 报文(SYN=1,序号=A),不包含应用层数据,封装于IP数据报发往服务器。
第二步:服务器收到 SYN 后,分配缓存和变量,回复 SYN+ACK 报文(SYN=1,ACK=1,确认号=A+1,序号=B)。该报文表示同意连接,并告知自己的初始序号B。
第三步:客户端收到 SYN+ACK 后,分配缓存和变量,发送 ACK 报文(ACK=1,SYN=0,确认号=B+1),并告知窗口大小。
TCP终止连接
第一步:客户端发出一个FIN报文段主动进行关闭连接,此时报文段的FIN标志位为1,假设序号为C,一般来说ACK标志也会被置1,但确认序号字段是无效的。
第二步:当服务器收到这个FIN报文段,它发回一个ACK报文段,确认序号为收到的序号加 1(C+1),和SYN一样,一个FIN将占用一个序号,此时断开客户端->服务器的方向连接。
第三步:服务器会向应用程序请求关闭与这个客户端的连接,接着服务器就会发送一个FIN报文段,此时假设序号为D,ACK标志虽然也为1,但是确认序号字段是无效的。
第四步:客户端返回一个ACK报文段来确认终止连接的请求,ACK标志置1,并将确认序号设置为收到序号加1(D+1),此时断开服务器->客户端的方向连接。
TCP状态
LISTENING:端口开放,等待连接请求。
SYN_SENT:客户端发送
SYN后等待匹配连接。SYN_RECEIVED:服务端收到
SYN并回复SYN+ACK,等待客户端ACK。ESTABLISHED:连接已建立,双方可交换数据。
FIN_WAIT_1与FIN_WAIT_2:主动关闭端发送
FIN后进入FIN_WAIT_1;收到ACK后进入FIN_WAIT_2,等待对方发送FIN。CLOSE_WAIT:被动关闭端收到
FIN,发送ACK,等待应用层关闭。TIME_WAIT:主动关闭端在收到
FIN后发送ACK,等待2MSL确保ACK到达。- MSL:最大报文段生存时间(Maximum Segment Lifetime),通常为2分钟。它确保旧连接中的报文段在网络中完全消失,避免影响后续新连接。
虚线:表示服务器的状态转移。
实线:表示客户端的状态转移。
图中所有"关闭”、“打开"都是应用程序主动处理。
图中所有的"超时"都是内核超时处理。
套接字
概念
套接字(Socket) 是一种进程间通信机制,并且支持跨计算机通信。与管道不同,它具有明确的客户端与服务端区分。
函数
socket:创建套接字。bind:把套接字和IP地址或端口号关联。connect:用于客户端主动连接服务器。listen:将socket设置为监听状态,等待客户端连接请求。accept:接受客户端连接请求。read:从已连接的套接字中读取数据。recv:从已连接的套接字中接收数据。- 与
read的区别:recv可以指定标志位(如MSG_PEEK、MSG_WAITALL等),从而控制数据接收的方式,而read不具备这些选项。此外,recv通常只用于套接字,而read可用于任何文件描述符。因此,recv提供了更精细的接收控制,适用于网络编程中的特殊需求。
- 与
write:向已连接的套接字发送数据。send:向已连接的套接字发送数据。- 与
write的区别:send可以指定标志位(如MSG_DONTROUTE、MSG_OOB等),从而控制数据发送的方式,而write不具备这些选项。此外,send通常只用于套接字,而write可用于任何文件描述符。因此,send提供了更精细的发送控制,适用于网络编程中的特殊需求。
- 与
sendto:用于向指定地址发送数据,主要用于无连接套接字(如UDP),可以指定目标地址和端口。- 与
send的区别:sendto能显式指定接收方地址,而send仅适用于已连接套接字。
- 与
close:关闭已连接的套接字。ioctl:用于设定套接字相关参数。getsockopt、setsockopt:用于获取/设置套接字选项。
TCP示例
客户端
客户端没有使用bind时,系统会自动分配一个临时端口。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#define HOST "192.168.0.217" // 根据你服务器的IP地址修改
#define PORT 6666 // 根据你服务器进程绑定的端口号修改
#define BUFFER_SIZ (4 * 1024) // 4k的数据区域
int main(void)
{
int sockfd, ret;
struct sockaddr_in server;
char buffer[BUFFER_SIZ]; //用于保存输入的文本
// 创建套接字描述符
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
printf("create an endpoint for communication fail!\n");
exit(1);
}
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(PORT); // htons 将主机字节序转换为网络字节序
server.sin_addr.s_addr = inet_addr(HOST); // inet_addr 将点分十进制 IP 字符串转为 32 位网络字节序整数
// 建立TCP连接
if (connect(sockfd, (struct sockaddr *)&server, sizeof(struct sockaddr)) == -1) {
printf("connect server fail...\n");
close(sockfd);
exit(1);
}
printf("connect server success...\n");
while (1) {
printf("please enter some text: ");
fgets(buffer, BUFFER_SIZ, stdin);
//输入了end,退出循环(程序)
if(strncmp(buffer, "end", 3) == 0)
break;
write(sockfd, buffer, sizeof(buffer));
}
close(sockfd);
exit(0);
}
服务端
#include <stdio.h>
#include <netdb.h>
#include <unistd.h>
#include <netinet/in.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#define MAX 10*1024
#define PORT 6666
// Driver function
int main()
{
char buff[MAX];
int n;
int sockfd, connfd, len;
struct sockaddr_in server, client;
// socket create and verification
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1) {
printf("socket creation failed...\n");
exit(0);
}
printf("socket successfully created..\n");
memset(&server, 0, sizeof(server));
// assign IP, PORT
server.sin_family = AF_INET;
server.sin_addr.s_addr = htonl(INADDR_ANY); // 绑定所有网卡
server.sin_port = htons(PORT);
// binding newly created socket to given IP and verification
if ((bind(sockfd, (struct sockaddr*)&server, sizeof(server))) != 0) {
printf("socket bind failed...\n");
exit(0);
}
printf("socket successfully binded..\n");
// now server is ready to listen and verification
if ((listen(sockfd, 5)) != 0) {
printf("Listen failed...\n");
exit(0);
}
printf("server listening...\n");
len = sizeof(client);
// accept the data packet from client and verification
connfd = accept(sockfd, (struct sockaddr*)&client, &len);
if (connfd < 0) {
printf("server acccept failed...\n");
exit(0);
}
printf("server acccept the client...\n");
// infinite loop for chat
while(1) {
memset(buff, 0, MAX);
// read the messtruct sockaddrge from client and copy it in buffer
if (read(connfd, buff, sizeof(buff)) <= 0) {
printf("client close...\n");
close(connfd);
break;
}
// print buffer which contains the client contents
printf("from client: %s\n", buff);
// if msg contains "Exit" then server exit and chat ended.
if (strncmp("exit", buff, 4) == 0) {
printf("server exit...\n");
close(connfd);
break;
}
}
// After chatting close the socket
close(sockfd);
exit(0);
}
网络IO
同步IO
在操作系统中,程序的运行空间分为内核空间和用户空间。用户空间对IO的所有操作都会转发到内核空间完成。 如果程序运行时发生了IO操作,系统就会挂起当前线程,此时该线程无法执行其他代码。这种运行方式成为同步IO。
异步IO
当程序需要进行IO操作时,它只是发出IO操作的命令,不等待结果返回而是执行其他操作。等到IO操作完成时,再通知CPU进行处理。这被称为异步IO。
阻塞IO
默认所有socket操作都是阻塞的。由于IO操作需要转发到内核空间,因此此时两个空间都被阻塞了。
非阻塞IO
如果设置socket为非阻塞模式,则当内核空间中没有IO数据时,会直接放回error。所以,非阻塞I/O的特点是用户进程需要不断的 轮询 内核空间的数据是否准备完成。
多路复用IO
select、poll、epoll等操作是多路复用IO。它们可以实现单个进程同时处理多个网络连接。它们是基于轮询的。相较于多线程/多进程,它的开销很小。
但select,poll,epoll本质上都是同步IO,因为他们都需要 在读写事件就绪后自己负责进行读写 ,也就是说这个读写过程是 阻塞 的,而 异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间 。
一般来说I/O复用多用于以下情况:
- 当客户处理多个描述符时。
- 服务器在高并发处理网络连接的时候。
- 服务器既要处理监听套接口,又要处理已连接套接口,一般也要用到I/O复用。
- 如果一个服务器即要处理TCP,又要处理UDP,一般要使用I/O复用。
- 如果一个服务器要处理多个服务或多个协议,一般要使用I/O复用。
注意:
select、poll、epoll不仅仅用于socket,还可以用于任何具有 「数据就绪」 概念的文件,如管道。
select
如果select监听的所有socket都不可读写,则阻塞。否则就返回所有可读写的socket的描述符。
缺点:
- 能监视的socket数量具有上限。
- 用来存放
fd的数据结构(位掩码)在内核空间和用户空间传递时开销大。 - 每当有活跃的socket时,都需遍历来找到目标描述符。时间复杂度为O(n)。
函数原型:
int select(int maxfdp1,fd_set *readset,fd_set *writeset,fd_set *exceptset,const struct timeval *timeout)
参数说明:
- maxfdp1:指定感兴趣的socket描述符个数,它的值是套接字最大socket描述符加1,socket描述符0、1、2 … maxfdp1-1均将被设置为感兴趣(即会查看他们是否可读、可写)。
- readset:指定这个socket描述符是可读的时候才返回。
- writeset:指定这个socket描述符是可写的时候才返回。
- exceptset:指定这个socket描述符是异常条件时候才返回。
- timeout:超时的时间。
poll
它和select非常相似。区别在于它使用链表存储文件描述符,因而不限制监视的fd数量。
函数原型:
int poll (struct pollfd *fds, unsigned int nfds, int timeout);
epoll
epoll使用一个 epfd (epoll文件描述符)管理多个socket描述符,epoll不限制socket描述符的个数,将用户空间的socket描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的复制只需一次。当socket激活时,它通过回调通知用户程序。因而找到目标描述符的复杂度为O(1)。它只管就绪的fd,而和fd总数无关。 epoll使用了 内存映射(mmap) 技术,使得内核与用户空间共享同一块内存,避免了数据在用户态和内核态之间的复制,从而大大提高了事件通知的效率。
├── 红黑树:保存所有注册进来的 fd
│ └── epoll_ctl 操作它
└── 就绪链表:保存已经发生事件的 fd
└── epoll_wait 从这里取事件
操作模式
epoll有两种操作模式:LT(Level Trigger) 和 ET(Edge Trigger)。LT为默认模式。
- LT:水平发出模式。当epoll_wait检测到socket可用时,通知应用程序。应用可以不立即处理,而且下次epoll_wait时还会再次通知。
- ET:边缘触发模式。应用必须立即处理,下次不会通知。
函数
epoll_create1
用于创建一个epfd。1G的内存大约能监视10万个端口。
int epoll_create1(int flags);
- epfd监视的端口上限来自
/proc/sys/fs/epoll/max_user_watches。 - flags 常用
EPOLL_CLOEXEC,当子进程执行exec时,这个 epoll 描述符就会被自动关闭,防止资源泄漏和多进程混乱。
epoll_ctl
用于控制某个epoll文件描述符上的事件,可以注册事件,修改事件,以及删除事件。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
- epfd
- op:操作的选项。
EPOLL_CTL_ADD:注册fd到epoll句柄中。EPOLL_CTL_MOD:修改已经注册的fd的监听事件。EPOLL_CTL_DEL:删除已经注册的socket描述符。
- fd:指定监听的socket描述符。
- event:event结构体如下:
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
- events可以是以下几个宏的集合:
EPOLLIN:表示对应的文件描述符可以读(包括对端SOCKET正常关闭)。EPOLLOUT:表示对应的文件描述符可以写。EPOLLPRI:表示对应的文件描述符有紧急的数据可读。EPOLLERR:表示对应的文件描述符发生错误。EPOLLHUP:表示对应的文件描述符被挂断。EPOLLET: 将EPOLL设为ET模式,这是相对于LT模式的。EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里。
epoll_wait
用于等待监听事件的发生。
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
- maxevents:告知内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的指定的size。
示例
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/epoll.h>
#include <sys/socket.h>
#define MAX_EVENTS 10
#define PORT 8888
#define BUFFER_SIZE 1024
int main() {
int listen_fd, epoll_fd;
struct sockaddr_in server_addr;
struct epoll_event ev, events[MAX_EVENTS];
// 1. 创建监听 socket
listen_fd = socket(AF_INET, SOCK_STREAM, 0);
if (listen_fd == -1) {
perror("socket failed");
exit(EXIT_FAILURE);
}
// 设置端口复用(防止服务器重启时提示 Address already in use)
int opt = 1;
setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(PORT);
// 2. 绑定并监听
if (bind(listen_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
perror("bind failed");
exit(EXIT_FAILURE);
}
if (listen(listen_fd, 10) == -1) {
perror("listen failed");
exit(EXIT_FAILURE);
}
printf("Server is listening on port %d...\n", PORT);
// 3. 创建 epoll 实例
epoll_fd = epoll_create1(0);
if (epoll_fd == -1) {
perror("epoll_create1 failed");
exit(EXIT_FAILURE);
}
// 4. 将监听 socket 加入 epoll 红黑树
ev.events = EPOLLIN; // 关注可读事件
ev.data.fd = listen_fd; // 关联文件描述符
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_fd, &ev) == -1) {
perror("epoll_ctl: listen_fd failed");
exit(EXIT_FAILURE);
}
// 5. 事件循环
while (1) {
// 等待事件发生(阻塞)
int nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
if (nfds == -1) {
perror("epoll_wait failed");
exit(EXIT_FAILURE);
}
// 处理所有就绪的事件
for (int i = 0; i < nfds; i++) {
// 情况 A:有新客户端连接请求
if (events[i].data.fd == listen_fd) {
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
int client_fd = accept(listen_fd, (struct sockaddr *)&client_addr, &client_len);
if (client_fd == -1) {
perror("accept failed");
continue;
}
printf("New client connected from %s:%d\n",
inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
// 将新连接的 client_fd 加入 epoll 监控
ev.events = EPOLLIN;
ev.data.fd = client_fd;
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &ev);
}
// 情况 B:已有客户端发来数据或断开连接
else {
int client_fd = events[i].data.fd;
char buffer[BUFFER_SIZE] = {0};
int bytes_read = read(client_fd, buffer, sizeof(buffer) - 1);
if (bytes_read <= 0) {
// bytes_read == 0 表示客户端正常关闭,< 0 表示出错
if (bytes_read == 0) {
printf("Client disconnected (fd: %d)\n", client_fd);
} else {
perror("read error");
}
// 从 epoll 中移除并关闭 socket
epoll_ctl(epoll_fd, EPOLL_CTL_DEL, client_fd, NULL);
close(client_fd);
} else {
// 正常收到数据,打印并原样回显(Echo)给客户端
printf("Received from fd %d: %s", client_fd, buffer);
write(client_fd, buffer, bytes_read);
}
}
}
}
// 清理资源(实际运行中上面是死循环,这里作示意)
close(listen_fd);
close(epoll_fd);
return 0;
}
当TCP服务器停止时,端口不会被立即释放,而是进入**TIME_WAIT** 状态。持续2个MSL(默认2分钟)后才会释放。因此需要setsockopt来设定SO_REUSEADDR,意为当端口处于TIME_WAIT时,新程序可以直接使用它。