01-网络通讯基础知识

1. Nginx的安装

-进入官网:www.nginx.org,找到download,选择一个版本(选择最新的稳定版本)
-使用wget下载到Linux中,解压缩,进入解压后的目录,执行 ./configure,此时生成了一个makefile文件,执行make,然后执行make install进行安装,默认安装位置为 /usr/local下,可以通过参数指定安装位置
-进入 /usr/local/nginx/sbin/,执行 ./nginx 就可以启动nginx

2. nginx整体结构、进程模型

-启动nginx后可以使用  ps -ef | grep nginx ,查看nginx的执行情况,nginx有一个master的主线程和多个工作线程,master是一个监控线程,当有work线程挂掉了之后,master会马上创建新的线程来代替
-在 /usr/local/nginx/conf/下有一个文件 nginx.conf,这是一个配置文件,在这个配置文件中可以配置 work 进程的数量
-在sbin中可以执行  ./nginx -?或者 ./nginx -h 查看一些选项,当配置文件发生更改时可以在线更新 (执行 ./nginx -s reload),停止 nginx 服务器(./nginx -s quit),强制停止(./nginx -s stop)
-nginx 还可以热更新和热回滚

多线程模型和多进程模型:
	多线程模型,资源开销小,但是稳定性差,一个线程发生异常会影响到其它线程
	多进程模型,资源开销大,但是稳定,一个进程发生异常不会影响到其它进程

3. 终端和进程的关系说

3.1 进程组

进程组是一组进程的集合,每个进程都属于一个进程组,每个进程组有一个进程组leader进程,进程组的ID(PGID)等于leader进程的ID。对大部分进程来说,它自己就是进程组的leader,并且进程组里面就只有它自己一个进程。

EG: 可以通过将信号发送给一个进程组,使进程组中的所有进程都收到该信号。 
kill -进程组号   (默认发送1号信号)

ps -eo pid,ppid,pgid,sid,tty,pgrp,comm,cmd|grep -E 'bash|PID|nginx'  //用于查看指定进程运行情况

3.2 session

  • 一个或多个进程组可以构成一个会话 (session)。
  • 一个会话中有一个领导进程(session leader)。会话领导进程的PID是会话的SID(session ID)。会话中的每个进程组称为一个工作(job)。会话可以有一个进程组成为会话的前台工作(foreground),而其他的进程组是后台工作(background)。每个会话可以连接一个控制终端(也可以不连接)。
  • 会话的意义在于将多个工作囊括在一个终端,并取其中的一个工作作为前台,来直接接收该终端的输入输出以及终端信号。 其他工作在后台运行。当我们打开多个终端窗口时,实际上就创建了多个终端会话。每个会话都会有自己的前台工作和后台工作。工作组和会话机制在Linux的许多地方应用。
EG:
-当用xshell连接到主机时,即创建了一个session。shell即是session的leader进程,随后shell里面运行的进程都将属于这个session,当shell退出后,该会话中的进程将退出。

-shell里面启动一个进程后,一般都会将该进程放到一个单独的进程组,然后该进程fork的所有进程都会属于该进程组,比如多进程的程序,它的所有进程都会属于同一个进程组,当在shell里面按下CTRL+C时,该程序的所有进程都会收到SIGINT而退出(只有前台进程组才会接受 CTRL+C)

3.3 前台任务

前台任务是独占命令行窗口的任务,只有运行完了或者手动中止该任务,才能执行其他命令。shell中启动一个进程时,默认情况下,该进程是一个前台进程组的leader,可以收到用户的输入,并且可以将输出打印到终端,只有当该进程组退出后,shell才可以再响应用户的输入。

3.4 后台任务

与前台任务相对应,后台任务在运行的时候,并不需要与用户交互,它们通常在不打扰用户其它工作的时候默默地执行。使shell可以继续响应用户的输入
后台任务继承当前会话的标准输出(stdout)和标准错误(stderr)。因此,后台任务的所有输出依然会同步地在命令行下显示。
不再继承当前session的标准输入(stdin),你无法向这个任务输入指令了。如果它试图读取标准输入,就会暂停执行(halt)。

可以看出,”后台任务”与”前台任务”的本质区别只有一个:是否继承标准输入。

3.5 前台任务与后台任务的切换

3.5.1 切换为后台任务

  • 启动时变为后台任务
    只要在命令的后面加上 & ,启动的进程就会成为后台进程。但是后台程序的输出仍然会打印到终端,影响用户输入。可以通过 ./程序>log.out &,将输出重定向到文件中。
  • 将正在运行的前台任务变为后台任务
    ctrl+z ,之后执行 bg 命令。相当于让最近一个暂停的“后台任务”继续执行。
CTRL+Z 和 CTRL+C的对比 
  CTRL+Z 和 CTRL+C 都是中断命令,但是他们的作用却不一样. CTRL+C 是强制中断程序的执行,而 CTRL+Z 的是将任务中断,但是此任务并没有结束,仍然在进程中,只是维持挂起的状态,用户可以使用 fg/bg 操作继续前台或后台的任务。

3.5.2 切换为前台进程

  • 当前终端没有被占用,那么执行 fg 命令就可以将一个后台进程拉到前台
可以使用 jobs 查看后台运行的进程组

3.6 NOHUP信号

当一个会话进程会结束后,这个进程会发送 nohup 信号给当前所有的进程,进程对nohup的默认处理动作为退出

验证上面的结论可以使用 strae 工具 
-执行【strace -e trace=signal -p pid】,这条命令可以绑定到一个进程对信号处理的情况
-用上面那条命令分别绑定会话进程和该会话中其它进程,然后杀死会话进程,查看情况

如果一个进程不想因为会话进程的死亡和退出:

  • 将这个进程变为守护进程
  • 该进程捕捉 sighup 信号,并进行处理,不使用默认的处理动作
  • 使用 setsid 或者 nohup 命令执行这个进程

4. 信号的概念

1.信号的概念
(1)信号就是一些数字(宏定义),通过 kill -l 就可以查看支持的信号类型,这些宏定义可以通过 find / -name "signal.h" | xargs grep -in "SIGHUP" 查看
(2)一般系统对每个信号有默认的动作(一般是退出)

2.信号处理的相关动作
2.1当某个信号出现时,我们可以按三种方式之一进行处理,我们称之为信号的处理或者与信号相关的动作;
(1)执行系统默认动作 ,绝大多数信号的默认动作是杀死你这个进程;
(2)忽略此信号(但是不包括SIGKILL和SIGSTOP)
(3)捕捉该信号:我写个处理函数,信号来的时候,我就用处理函数来处理;(但是不包括SIGKILL和SIGSTOP)

kill 命令发出的常见信号

kill的参数 该参数发出的信号 操作系统缺省动作
-1 SIGHUP(连接断开) 终止掉进程(进程没了)
-2 SIGINT(终端中断符,比如ctrl+c) 终止掉进程(进程没了)
-3 SIGQUIT(终端退出符,比如ctrl+\) 终止掉进程(进程没了)
-9 SIGKILL**(终止)** 终止掉进程(进程没了)
-18 SIGCONT(使暂停的进程继续) 忽略(进程依旧在运行不受影响)
-19 SIGSTOP**(停止),可用SIGCONT****继续,但任务被放到了后台** 停止进程(不是终止,进程还在
-20 SIGTSTP(终端停止符,比如ctrl+z),但任务被放到了后台,可用SIGCONT继续 停止进程(不是终止,进程还在

进程状态:

状态 含义
D 不可中断的休眠状态(通常是I/O的进程),可以处理信号,有 延迟
R 可执行状态&运行状态(在运行队列里的状态)
S 可中断的休眠状态之中(等待某事件完成),可以处理信号
T 停止或被追踪(被作业控制信号所停止)
Z 僵尸进程
X 死掉的进程
< 高优先级的进程
N 低优先级的进程
L 有些页被锁进内存
s Session leader(进程的领导者),在它下面有子进程
t 追踪期间被调试器所停止
+ 位于前台的进程组

常用信号列举:

信号名 信号含义
SIGHUP(连接断开) 是终端断开信号,如果终端接口检测到一个连接断开,发送此信号到该终端所在的会话首进程(前面讲过),缺省动作会导致所有相关的进程退出(上节课也重点讲了这个信号,xshell断开就有这个信号送过来); Kill -1 进程号也能发送此信号给进程;
SIGALRM(定时器超时) 一般调用系统函数alarm创建定时器,定时器超时了就会这个信号;
SIGINT(中断) 从键盘上输入ctrl+C(中断键)【比如你进程正跑着循环干一个事】,这一ctrl+C就能打断你干的事,终止进程; 但shell****会将后台进程对该信号的处理设置为忽略(也就是说该进程若在后台运行则不会收到该信号)
SIGSEGV(无效内存) 内存访问异常,除数为0等,硬件会检测到并通知内核;其实这个SEGV代表段违例(segmentation violation),你有的时候运行一个你编译出来的可执行的c程序,如果内存有问题,执行的时候就会出现这个提示;
SIGIO(异步I/O) 通用异步I/O信号,咱们以后学通讯的时候,如果通讯套接口上有数据到达,或发生一些异步错误,内核就会通知我们这个信号;
SIGCHLD(子进程改变) 一个进程终止或者停止时,这个信号会被发送给父进程;(我们想象下nginx,worker进程终止时 master进程应该会收到内核发出的针对该信号的通知);
SIGUSR1,SIGUSR2(都是用户定义信号) 用户定义的信号,可用于应用程序,用到再说;
SIGTERM(终止) 一般你通过在命令行上输入kill命令来杀一个进程的时候就会触发这个信号,收到这个信号后,你有机会退出前的处理,实现这种所谓优雅退出的效果;
SIGKILL(终止) 不能被忽略,这是杀死任意进程的可靠方法,不能被进程本身捕捉
SIGSTOP(停止) 不能被忽略,使进程停止运行,可以用SIGCONT继续运行,但进程被放入到了后台
SIGQUIT(终端退出符) 从键盘上按ctrl+\ 但shell****会将后台进程对该信号的处理设置为忽略(也就是说该进程若在后台运行则不会收到该信号)
SIGCONT(使暂停进程继续) 使暂停的进程继续运行
SIGTSTP(终端停止符) 从键盘上按ctrl+z,进程被停止,并被放入后台,可以用SIGCONT继续运行

5. 信号编程初步

5.1 Unix/Linux操作系统体系结构

(1)类Unix操作系统体系结构分为两个状态 (1)用户态,(2)内核态
	用户态权限小,内核态权限大,

(2)系统调用对内核进行了封装并暴露一些接口供外层调用

(3)shell:   bash(borne again shell[重新装配的shell]),它是shell的一种,linux上默认采用的是bash这种shell

(4)用户态,内核态之间的切换
-运行于用户态的进程可以执行的操作和访问的资源会受到极大限制(用户态权限小);
-而运行在内核态的进程可以执行任何操作并且在资源的使用上没有限制(内核态权限大);
-一个进程执行的时候,大部分时间是处于用户态下的,只有需要内核所提供的服务时 才会切换到内核态,内核态做的事情完成后,又转回到用户态,这种转换是由内核控制的,不需要我们关心

(5)疑问:为什么要区分用户态,内核态;
-大概有两个目的:
	(1)一般情况下,程序都运行在用户态状态,权限小,不至于危害到系统其它部分;当你干一些危险的事情的时候,系统给你提供接口,让你去干;
	(2)既然这些接口是系统提供给你的,那么这些接口也是操作系统统一管理的;
-资源是有限的, 如果大家都来访问这些资源,如果不加以管理,一个是访问冲突,一个是被访问的资源如果耗尽,那系统还可能崩溃;
-系统提供这些接口,就是为了减少有限的资源的访问以及使用上冲突;

(6)那么什么时候从用户态切换到内核态去呢?
-系统调用,比如调用malloc();
-异常事件,比如来了个信号;
-外围设备中断:

在这里插入图片描述

5.2 signal函数范例

(1)可重入函数
可重入函数:就是我们在信号处理函数中 调用它 是安全的,在信号处理程序中保证调用安全的函数,这些函数是可重入的并被称为异步信号安全的;
eg:有一些大家周知的函数都是不可重入的,比如malloc(),printf();


​ (2)写信号处理函数的时候,要注意的事项:
​ -如果必须要在信号处理函数中调用一些系统函数,那么要保证在信号处理函数中调用的 系统函数一定要是可重入的
​ -如果必须要在信号处理函数中调用那些可能修改errno值的可重入的系统函数,那么 就得事先备份errno值,从信号处理函数返回之前,将errno值恢复;

(3)信号捕捉函数 signal,不遵循posix标准,不建议使用,建议使用 sigaction

6. 信号编程进阶

6.1 信号集

  • 在PCB中有两个非常重要的信号集。一个称之为“阻塞信号集”,另一个称之为“未决信号集”。这两个信号集都是内核使用位图机制来实现的

  • 信号的 “未决” 是一种状态,指的是从信号的产生到信号被处理前的这一段时间

  • 信号的 “阻塞” 是一个开关动作,指的是阻止信号被处理,但不是阻止信号产生

  • 信号的阻塞就是让系统暂时保留信号留待以后发送由于另外有办法让系统忽略信号,所以一般情况下信号的阻塞只是暂时的,只是为了 防止信号打断敏感的操作。

  • 阻塞信号集默认是不阻塞的(全为0)

在这里插入图片描述

6.2 信号集相关函数

// 前四个函数返回0->成功, -1->失败
#include <signal.h>
// 清空信号集
// 信号集所有的标志位的值设置为0
int sigemptyset(sigset_t *set);
	- set: 自定义的信号集
// 信号集所有的标志位的值设置为1
int sigfillset(sigset_t *set);
// 在信号集中, 添加阻塞的信号
int sigaddset(sigset_t *set, int signum);
	- set: 自定义信号集
	- signum: 将那个信号设置到信号集中, 阻塞这个信号
// 将某个信号从设置好的信号集中删除
int sigdelset(sigset_t *set, int signum);

// 判断某个信号是否已经设置到了自定义的信号集中
int sigismember(const sigset_t *set, int signum);
	- set: 自定义的信号集
	- signum: 某一个信号的编号/信号对应的宏
	返回值:
		1: 判断的信号在信号集中
		0: 判断的信号没有在信号集中
		-1: 失败
    - 注意,信号集的下标是从1开始的
	
// 将自定义信号中的数据设置到内核中
// 修改是内核的阻塞信号集
int sigprocmask(int how, const sigset_t *newset, sigset_t *oldset);
参数: 
- how: 如何对内核阻塞信号集进行处理
	SIG_BLOCK: 该值代表的功能是将newset所指向的信号集中所包含的信号加到当前的信号掩码中,作为新的信号屏蔽字
	SIG_UNBLOCK: 将参数newset所指向的信号集中的信号从当前的信号掩码中移除
	SIG_SETMASK: 设置当前信号掩码为参数newset所指向的信号集中所包含的信号
-执行成功返回0,失败返回-1       
-错误代码
	EFAULT:参数set,oldset指针地址无法存取
	EINTR:此调用被中断

// 读内核的未决信号集
int sigpending(sigset_t *set);
	- set: 传出参数, 保存了内核未决信号集中的信息

6.3 信号捕捉

6.3.1 signal

#include <signal.h>
// int型参数 -> 捕捉到的信号的编号
typedef void (*sighandler_t)(int);

sighandler_t signal(int signum, sighandler_t handler);
	参数: 
		- signum: 要捕捉的信号
		- handler: 回调函数

6.3.2 sigaction

#include <signal.h>
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_flags = 0,使用 sa_handler 这种类型回调函数(默认为0)
sa_flags = SA_SIGINFO, 使用sa_sigaction

int sigaction(int signum, const struct sigaction *act,
                     struct sigaction *oldact);	
	参数: 
	 - signum: 要捕捉的信号
	 - act: 捕捉到信号之后的处理动作
	 - oldact: 上一次对信号捕捉的相关设置, 不使用-> NULL
	返回值: 
	  成功: 0, 失败: -1

推荐使用sigaction,因为sigaction完全遵循posix标准,而signal不是严格遵守posix标准导致可能在不同的系统上执行结果不一样

7. execl函数族

exec函数族的作用是根据指定的文件名找到可执行文件,并用它来取代调用进程的内容,换句话说,就是在调用进程内部执行一个可执行文件。

exec函数族的函数执行成功后不会返回,因为调用进程的实体,包括代码段数据段堆栈等都已经被新的内容取代,只留下进程ID等一些表面上的信息仍保持原样,颇有些神似“三十六计”中的“金蝉脱壳”。看上去还是旧的躯壳,却已经注入了新的灵魂。只有调用失败了,它们才会返回一个-1,从原程序的调用点接着往下执行。

#include <unistd.h>
extern char **environ;
// 不是linux系统函数
int execl(const char *path, const char *arg, ...);  // 使用最多的
	参数: 
		- path: 可执行程序的路径, 建议写绝对路径
		- arg: 第二个参数, 随便写, 一般为了看起来舒服, 写成和参数1相同的值
		- 从第三个参数: 可执行程序执行过程中需要的真正的参数
		- 最后一个参数: NULL(哨兵)
   举例: /home/itcast/test/a.out
   execl("/home/itcast/test/a.out", "a.out", "hello", "123", NULL);
   execl("/bin/ps", "ps", "a", "u", "x", NULL);

int execlp(const char *file, const char *arg, ...); // 使用最多的
	参数:
		- file: 可执行程序的名字, 在执行这个程序之前, 子动搜索系统环境变量PATH
		- arg: 第二个参数, 随便写, 一般为了看起来舒服, 写成和参数1相同的值
		- 从第三个参数: 可执行程序执行过程中需要的真正的参数
		- 最后一个参数: NULL(哨兵)
int execle(const char *path, const char *arg, ..., char *const envp[]);
	- path: 文件名
	- char *const envp[], 从这个参数指定的路径中所属第一个参数对应的文件名
	char* envp[] = {"/home/robin", "/a/b", "home/zhangsan/test", NULL};

int execv(const char *path, char *const argv[]);
	- path: 例如: /bin/ps
	- argv: 参数列表
	
	char* args[] = {"xxx", "aux", NULL};
int execvp(const char *file, char *const argv[]);
// linux系统函数
int execve(const char *path, char *const argv[], char *const envp[]);	
l(list) 参数地址列表,以空指针结尾
v(vector) 存有各参数地址的指针数组的地址
p(path) 按 PATH 环境变量指定的目录搜索可执行文件
e(environment) 存有环境变量字符串地址的指针数组的地址

8. 子进程回收

8.1 wait及waitpid

#include<sys/types.h>
#include<sys/wait.h>

pid_t wait(int *status);
- 参数: status记录了子进程退出时候的状态, 正常->退出值(exit(10))/非正常退出 -> 被信号干掉了
- 返回值: 
	大于0: 被回收的子进程的进程ID
	等于-1: 调用失败
// status参数的使用
	int s;
    int ret = wait(&s);
    if(WIFEXITED(s))	// 判断子进程是不是正常退出
    {   
       printf("退出的状态码: %d\n", WEXITSTATUS(s));	// 打印子进程退出的状态码
    }   
    if(WIFSIGNALED(s))	// 判断子进程是不是被信号干掉了
    {   
        printf("被这个信号干掉了: %d\n", WTERMSIG(s));	// 打印子进程是被那个信号杀死的
    }   	


pid_t waitpid(pid_t pid,int * status,int options);
-pid
	pid<-1 等待进程组识别码为 pid 绝对值的任何子进程。
	pid=-1 等待任何子进程,相当于 wait()。
	pid=0 等待进程组识别码与目前进程相同的任何子进程。
	pid>0 等待任何子进程识别码为 pid 的子进程。
status:
	子进程的状态,一般传入空
-options
	如果不想使用options,也可以把options设为0
    WNOHANG 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若结束,则返回该子进程的ID,如果此时只剩下父进程则返回-1

8.2 SIGCHLD信号

8.2.1 SIGCHLD的产生条件

  1. 子进程死了, 自杀, 他杀
  2. 子进程被暂停
  3. 子进程由暂停状态重新恢复运行

以上三种情况子进程都会给父进程发送信号: SIGCHLD, 父进程默认会忽略这个信号

8.2.2 使用SIGCHLD回收子进程

// 回调函数
void recycle(int num)
{
    printf("catch num: %d\n", num);
    // 回收子进程资源
    while(1)
    {
        int ret = waitpid(-1, NULL, WNOHANG);
        if(ret > 0)
        {
            printf("child die, pid = %d\n", ret);
        }
        else if(ret == 0)
        {
            // 死了的子进程回收完成
            break;
        }
        else if(ret == -1)
        {
            printf("所有的子进程死光了, 回收完成!\n");
            break;
        }
    }
}

-父进程捕捉 SIGCHLD信号,设置回调函数为上述代码

8.3 fork 面试题

fork()&&fork()||fork()&&fork()
-上一行程序执行完之后会产生多少个进程,7

9. 守护进程

9.1 dup于dup2

#include <unistd.h>

int dup(int oldfd); // 复制文件描述符
EG: int ret = dup(3);
// 假设值等于3的文件描述符指向一个文件a.txt
// 返回值是从空闲的文件描述符表中找到的最小的一个(假设最小的是4), 这时候 4 指向a.txt

int dup2(int oldfd, int newfd);
//dup2函数原型 dup2(fd,fd1); 执行这个函数,首先会切断fd1和文件的联系然后将fd1指向fd所指向的文件,fd必须是一个有效的文件描述符,不然没有意义

9.2 守护进程示例

#include <stdio.h>
#include <stdlib.h>  //malloc
#include <unistd.h>
#include <signal.h>

#include <sys/stat.h>
#include <fcntl.h>

//创建守护进程
//创建成功则返回1,否则返回-1
int ngx_daemon()
{
    int  fd;

    switch (fork())  //fork()子进程
    {
    case -1:
        //创建子进程失败,这里可以写日志......
        return -1;
    case 0:
        //子进程,走到这里,直接break;
        break;
    default:
        //父进程,直接退出 
        exit(0);         
    }

    //只有子进程流程才能走到这里
    if (setsid() == -1)  //脱离终端,终端关闭,将跟此子进程无关
    {
        //记录错误日志......
        return -1;
    }
    umask(0); //设置为0,不要让它来限制文件权限,以免引起混乱

    fd = open("/dev/null", O_RDWR); //打开黑洞设备,以读写方式打开
    if (fd == -1) 
    {
        //记录错误日志......
        return -1;
    }
    if (dup2(fd, STDIN_FILENO) == -1) //先关闭STDIN_FILENO[这是规矩,已经打开的描述符,动他之前,先close],类似于指针指向null,让/dev/null成为标准输入;
    {
        //记录错误日志......
        return -1;
    }

    if (dup2(fd, STDOUT_FILENO) == -1) //先关闭STDIN_FILENO,类似于指针指向null,让/dev/null成为标准输出;
    {
        //记录错误日志......
        return -1;
    }

     if (fd > STDERR_FILENO)  //fd应该是3,这个应该成立
     {
        if (close(fd) == -1)  //释放资源这样这个文件描述符就可以被复用;不然这个数字【文件描述符】会被一直占着;
        {
            //记录错误日志......
            return -1;
        }
    }

    return 1;
}

int main(int argc, char *const *argv)
{
    if(ngx_daemon() != 1)
    {
        //创建守护进程失败,可以做失败后的处理比如写日志等等
        return 1; 
    } 
    else
    {
        //创建守护进程成功,执行守护进程中要干的活
        for(;;)
        {        
            sleep(1); //休息1秒
            printf("休息1秒,进程id=%d!\n",getpid()); //你就算打印也没用,现在标准输出指向黑洞(/dev/null),打印不出任何结果【不显示任何结果】
        }
    }
    return 0;
}
发布了61 篇原创文章 · 获赞 31 · 访问量 10万+

猜你喜欢

转载自blog.csdn.net/qq_40794602/article/details/105475586