ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

从零构建通讯器--4.4-4.5信号在创建线程的实战作用、write函数写入日志设置成不混乱、文件IO详解

2021-05-01 19:02:16  阅读:272  来源: 互联网

标签:4.5 4.4 通讯器 worker process 信号 进程 sa ngx


(1)信号功能实战

①signal():注册信号处理程序的函数;
商业软件中,不用signal(),而要用sigaction();
②信号初始化函数:
ngx_init_signals()
现在只是循环遍历,有信号就交给信号处理函数
③信号处理函数:
ngx_signal_handler(int signo, siginfo_t *siginfo, void *ucontext)
现在只是有信号就打印有信号来了
效果:
在这里插入图片描述
④ngx_signal.cxx
代码:

//和信号有关的函数放这里
#include <string.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>    //信号相关头文件 
#include <errno.h>     //errno

#include "ngx_macro.h"
#include "ngx_func.h" 

//一个信号有关的结构 ngx_signal_t
typedef struct 
{
    int           signo;       //信号对应的数字编号 ,每个信号都有对应的#define ,大家已经学过了 
    const  char   *signame;    //信号对应的中文名字 ,比如SIGHUP 

    //信号处理函数,这个函数由我们自己来提供,但是它的参数和返回值是固定的【操作系统就这样要求】,大家写的时候就先这么写,也不用思考这么多;
    void  (*handler)(int signo, siginfo_t *siginfo, void *ucontext); //函数指针,   siginfo_t:系统定义的结构
} ngx_signal_t;

//声明一个信号处理函数
static void ngx_signal_handler(int signo, siginfo_t *siginfo, void *ucontext); //static表示该函数只在当前文件内可见

//数组 ,定义本系统处理的各种信号,我们取一小部分nginx中的信号,并没有全部搬移到这里,日后若有需要根据具体情况再增加
//在实际商业代码中,你能想到的要处理的信号,都弄进来
ngx_signal_t  signals[] = {
    // signo      signame             handler
    { SIGHUP,    "SIGHUP",           ngx_signal_handler },        //终端断开信号,对于守护进程常用于reload重载配置文件通知--标识1
    { SIGINT,    "SIGINT",           ngx_signal_handler },        //标识2   
	{ SIGTERM,   "SIGTERM",          ngx_signal_handler },        //标识15
    { SIGCHLD,   "SIGCHLD",          ngx_signal_handler },        //子进程退出时,父进程会收到这个信号--标识17
    { SIGQUIT,   "SIGQUIT",          ngx_signal_handler },        //标识3
    { SIGIO,     "SIGIO",            ngx_signal_handler },        //指示一个异步I/O事件【通用异步I/O信号】
    { SIGSYS,    "SIGSYS, SIG_IGN",  NULL               },        //我们想忽略这个信号,SIGSYS表示收到了一个无效系统调用,如果我们不忽略,进程会被操作系统杀死,--标识31
                                                                  //所以我们把handler设置为NULL,代表 我要求忽略这个信号,请求操作系统不要执行缺省的该信号处理动作(杀掉我)
    //...日后根据需要再继续增加
    { 0,         NULL,               NULL               }         //信号对应的数字至少是1,所以可以用0作为一个特殊标记
};

//初始化信号的函数,用于注册信号处理程序
//返回值:0成功  ,-1失败
int ngx_init_signals()
{
    ngx_signal_t      *sig;  //指向自定义结构数组的指针 
    struct sigaction   sa;   //sigaction:系统定义的跟信号有关的一个结构,我们后续调用系统的sigaction()函数时要用到这个同名的结构

    for (sig = signals; sig->signo != 0; sig++)  //将signo ==0作为一个标记,因为信号的编号都不为0;不等于0表示有信号,就继续遍历
    {        
        //我们注意,现在要把一堆信息往 变量sa对应的结构里弄 ......
        memset(&sa,0,sizeof(struct sigaction));

        if (sig->handler)  //如果信号处理函数不为空,这当然表示我要定义自己的信号处理函数
        {
            sa.sa_sigaction = sig->handler;  //sa_sigaction:指定信号处理程序(函数),注意sa_sigaction也是函数指针,是这个系统定义的结构sigaction中的一个成员(函数指针成员);
            sa.sa_flags = SA_SIGINFO;        //sa_flags:int型,指定信号的一些选项,设置了该标记(SA_SIGINFO),就表示信号附带的参数可以被传递到信号处理函数中
                                                //说白了就是你要想让sa.sa_sigaction指定的信号处理程序(函数)生效,你就把sa_flags设定为SA_SIGINFO
        }
        else
        {   
            //由于信号处理函数为空,所以要把把信号勿略掉
            sa.sa_handler = SIG_IGN; //sa_handler:这个标记SIG_IGN给到sa_handler成员,表示忽略信号的处理程序,否则操作系统的缺省信号处理程序很可能把这个进程杀掉;
                                      //其实sa_handler和sa_sigaction都是一个函数指针用来表示信号处理程序。只不过这两个函数指针他们参数不一样, sa_sigaction带的参数多,信息量大,
                                       //而sa_handler带的参数少,信息量少;如果你想用sa_sigaction,那么你就需要把sa_flags设置为SA_SIGINFO;                                       
        } //end if

        //不准备阻塞任何信号
        sigemptyset(&sa.sa_mask);   //比如咱们处理某个信号比如SIGUSR1信号时不希望收到SIGUSR2信号,那咱们就可以用诸如sigaddset(&sa.sa_mask,SIGUSR2);这样的语句针对信号为SIGUSR1时做处理,这个sigaddset三章五节讲过;
                                    //这里.sa_mask是个信号集(描述信号的集合),用于表示要阻塞的信号,sigemptyset()这个函数咱们在第三章第五节讲过:把信号集中的所有信号清0,本意就是不准备阻塞任何信号;
                                    
        
        //设置信号处理动作(信号处理函数),说白了这里就是让这个信号来了后调用我的处理程序,有个老的同类函数叫signal,不过signal这个函数被认为是不可靠信号语义,不建议使用,大家统一用sigaction
        if (sigaction(sig->signo, &sa, NULL) == -1) //参数1:要操作的信号
                                                     //参数2:主要就是那个信号处理函数以及执行信号处理函数时候要屏蔽的信号等等内容
                                                      //参数3:返回以往的对信号的处理方式【跟sigprocmask()函数边的第三个参数是的】,跟参数2同一个类型,我们这里不需要这个东西,所以直接设置为NULL;
        {   
            ngx_log_error_core(NGX_LOG_EMERG,errno,"sigaction(%s) failed",sig->signame); //显示到日志文件中去的 
            return -1; //有失败就直接返回
        }	
        else
        {            
            //ngx_log_error_core(NGX_LOG_EMERG,errno,"sigaction(%s) succed!",sig->signame);     //成功不用写日志 
            ngx_log_stderr(0,"sigaction(%s) succed!",sig->signame); //直接往屏幕上打印看看 ,不需要时可以去掉
        }
    } //end for
    return 0; //成功    
}

//信号处理函数
static void ngx_signal_handler(int signo, siginfo_t *siginfo, void *ucontext)
{
    printf("来信号了\n");
}

(2)Nginx中创建worker子进程

官方nginx ,一个master进程,创建了多个worker子进程,worker子进程数目配置成和cpu数目相同;创建进程的代码都放在ngx_process_cycle.cxx,在proc目录下;proc目录下的makefile生成.o文件即可,主目录makefile会链接
(2.0)看ngx_process_cycle.cxx如何创建子进程:
不管父进程还是子进程,正常工作期间都在这个函数里循环;
ngx_master_process_cycle()
①在创建子进程时,不受下列信号的干扰
②设置主进程标题,打印maser进程
ngx_setproctitle() //设置master进程标题
③读配置文件了解进程数目,创建worker子进程:ngx_start_worker_processes()
******只有父进程才会继续走下来,一直做for(;;)循环
④master进程在ngx_start_worker_processes循环创建子进程
然后父进程创建进程后回去了,进入for不断循环
在ngx_worker_process_cycle里面是子进程做的事
⑤ngx_worker_process_cycle
清空信号集,允许接受所有信号
重新为子进程设置标题,跟master进程群分开
进入for(;;)死循环
效果图:
在这里插入图片描述
全部杀掉用kill -9 -1344(组的id)
代码补充:
ngx_process_cycle.cxx

//和开启子进程相关

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <signal.h>   //信号相关头文件 
#include <errno.h>    //errno
#include <unistd.h>

#include "ngx_func.h"
#include "ngx_macro.h"
#include "ngx_c_conf.h"

//函数声明
static void ngx_start_worker_processes(int threadnums);
static int ngx_spawn_process(int threadnums,const char *pprocname);
static void ngx_worker_process_cycle(int inum,const char *pprocname);
static void ngx_worker_process_init(int inum);

//变量声明
static u_char  master_process[] = "master process";

//描述:创建worker子进程
void ngx_master_process_cycle()
{    
    sigset_t set;        //信号集

    sigemptyset(&set);   //清空信号集

    //下列这些信号在执行本函数期间不希望收到【考虑到官方nginx中有这些信号,老师就都搬过来了】(保护不希望由信号中断的代码临界区)
    //建议fork()子进程时学习这种写法,防止信号的干扰;
    //在创建子进程时,不受下列信号的干扰
    sigaddset(&set, SIGCHLD);     //子进程状态改变
    sigaddset(&set, SIGALRM);     //定时器超时
    sigaddset(&set, SIGIO);       //异步I/O
    sigaddset(&set, SIGINT);      //终端中断符
    sigaddset(&set, SIGHUP);      //连接断开
    sigaddset(&set, SIGUSR1);     //用户定义信号
    sigaddset(&set, SIGUSR2);     //用户定义信号
    sigaddset(&set, SIGWINCH);    //终端窗口大小改变
    sigaddset(&set, SIGTERM);     //终止
    sigaddset(&set, SIGQUIT);     //终端退出符
    //.........可以根据开发的实际需要往其中添加其他要屏蔽的信号......
    
    //设置,此时无法接受的信号;阻塞期间,你发过来的上述信号,多个会被合并为一个,暂存着,等你放开信号屏蔽后才能收到这些信号。。。
    //sigprocmask()在第三章第五节详细讲解过
    if (sigprocmask(SIG_BLOCK, &set, NULL) == -1) //第一个参数用了SIG_BLOCK表明设置 进程 新的信号屏蔽字 为 “当前信号屏蔽字 和 第二个参数指向的信号集的并集
    {        
        ngx_log_error_core(NGX_LOG_ALERT,errno,"ngx_master_process_cycle()中sigprocmask()失败!");
    }
    //即便sigprocmask失败,程序流程 也继续往下走

    //首先我设置主进程标题---------begin
    size_t size;
    int    i;
    size = sizeof(master_process);  //注意我这里用的是sizeof,所以字符串末尾的\0是被计算进来了的
    size += g_argvneedmem;          //argv参数长度加进来    
    if(size < 1000) //长度小于这个,我才设置标题
    {
        char title[1000] = {0};
        strcpy(title,(const char *)master_process); //"master process"
        strcat(title," ");  //跟一个空格分开一些,清晰    //"master process "
        for (i = 0; i < g_os_argc; i++)         //"master process ./nginx"
        {
            strcat(title,g_os_argv[i]);
        }//end for
        ngx_setproctitle(title); //设置标题
    }
    //首先我设置主进程标题---------end
        
    //从配置文件中读取要创建的worker进程数量
    CConfig *p_config = CConfig::GetInstance(); //单例类
    int workprocess = p_config->GetIntDefault("WorkerProcesses",1); //从配置文件中得到要创建的worker进程数量
    ngx_start_worker_processes(workprocess);  //这里要创建worker子进程

    //创建子进程后,父进程的执行流程会返回到这里,子进程不会走进来    
    sigemptyset(&set); //信号屏蔽字为空,表示不屏蔽任何信号
    
    for ( ;; ) 
    {

    //    usleep(100000);
        ngx_log_error_core(0,0,"haha--这是父进程,pid为%P",ngx_pid);

        //a)根据给定的参数设置新的mask 并 阻塞当前进程【因为是个空集,所以不阻塞任何信号】
        //b)此时,一旦收到信号,便恢复原先的信号屏蔽【我们原来的mask在上边设置的,阻塞了多达10个信号,从而保证我下边的执行流程不会再次被其他信号截断】
        //c)调用该信号对应的信号处理函数
        //d)信号处理函数返回后,sigsuspend返回,使程序流程继续往下走
        //printf("for进来了!\n"); //发现,如果print不加\n,无法及时显示到屏幕上,是行缓存问题,以往没注意;可参考https://blog.csdn.net/qq_26093511/article/details/53255970

//        sigsuspend(&set); //阻塞在这里,等待一个信号,此时进程是挂起的,不占用cpu时间,只有收到信号才会被唤醒(返回);
                         //此时master进程完全靠信号驱动干活    

//        printf("执行到sigsuspend()下边来了\n");
        
///        printf("master进程休息1秒\n");      
        //sleep(1); //休息1秒        
        //以后扩充.......

    }// end for(;;)
    return;
}

//描述:根据给定的参数创建指定数量的子进程,因为以后可能要扩展功能,增加参数,所以单独写成一个函数
//threadnums:要创建的子进程数量
static void ngx_start_worker_processes(int threadnums)
{
    int i;
    for (i = 0; i < threadnums; i++)  //master进程在走这个循环,来创建若干个子进程
    {
        ngx_spawn_process(i,"worker process");
    } //end for
    return;
}

//描述:产生一个子进程
//inum:进程编号【0开始】
//pprocname:子进程名字"worker process"
static int ngx_spawn_process(int inum,const char *pprocname)
{
    pid_t  pid;

    pid = fork(); //fork()系统调用产生子进程
    switch (pid)  //pid判断父子进程,分支处理
    {  
    case -1: //产生子进程失败
        ngx_log_error_core(NGX_LOG_ALERT,errno,"ngx_spawn_process()fork()产生子进程num=%d,procname=\"%s\"失败!",inum,pprocname);
        return -1;

    case 0:  //子进程分支
        ngx_parent = ngx_pid;              //因为是子进程了,所有原来的pid变成了父pid
        ngx_pid = getpid();                //重新获取pid,即本子进程的pid
        ngx_worker_process_cycle(inum,pprocname);    //我希望所有worker子进程,在这个函数里不断循环着不出来,也就是说,子进程流程不往下边走;
        break;

    default: //这个应该是父进程分支,直接break;,流程往switch之后走        
        break;
    }//end switch

    //父进程分支会走到这里,子进程流程不往下边走-------------------------
    //若有需要,以后再扩展增加其他代码......
    return pid;
}

//描述:worker子进程的功能函数,每个woker子进程,就在这里循环着了(无限循环【处理网络事件和定时器事件以对外提供web服务】)
//     子进程分叉才会走到之类
//inum:进程编号【0开始】
static void ngx_worker_process_cycle(int inum,const char *pprocname) 
{
    //重新为子进程设置进程名,不要与父进程重复------
    ngx_worker_process_init(inum);
    ngx_setproctitle(pprocname); //设置标题   

    //暂时先放个死循环,我们在这个循环里一直不出来
    //setvbuf(stdout,NULL,_IONBF,0); //这个函数. 直接将printf缓冲区禁止, printf就直接输出了。
    for(;;)
    {

        //先sleep一下 以后扩充.......
        //printf("worker进程休息1秒");       
        //fflush(stdout); //刷新标准输出缓冲区,把输出缓冲区里的东西打印到标准输出设备上,则printf里的东西会立即输出;
        //sleep(1); //休息1秒       
        //usleep(100000);
        ngx_log_error_core(0,0,"good--这是子进程,编号为%d,pid为%P!",inum,ngx_pid);
        //printf("1212");
        //if(inum == 1)
        //{
            //ngx_log_stderr(0,"good--这是子进程,编号为%d,pid为%P",inum,ngx_pid); 
            //printf("good--这是子进程,编号为%d,pid为%d\r\n",inum,ngx_pid);
            //ngx_log_error_core(0,0,"good--这是子进程,编号为%d",inum,ngx_pid);
            //printf("我的测试哈inum=%d",inum++);
            //fflush(stdout);
        //}
            
        //ngx_log_stderr(0,"good--这是子进程,pid为%P",ngx_pid); 
        //ngx_log_error_core(0,0,"good--这是子进程,编号为%d,pid为%P",inum,ngx_pid);

    } //end for(;;)
    return;
}

//描述:子进程创建时调用本函数进行一些初始化工作
static void ngx_worker_process_init(int inum)
{
    sigset_t  set;      //信号集

    sigemptyset(&set);  //清空信号集
    if (sigprocmask(SIG_SETMASK, &set, NULL) == -1)  //原来是屏蔽那10个信号【防止fork()期间收到信号导致混乱】,现在不再屏蔽任何信号【接收任何信号】
    {
        ngx_log_error_core(NGX_LOG_ALERT,errno,"ngx_worker_process_init()中sigprocmask()失败!");
    }

    
    //....将来再扩充代码
    //....
    return;
}

(2.1)sigsuspend()函数讲解
①在父进程的for循环出,阻塞在这里,等待一个信号,此时进程是挂起的,不占用cpu时间,只有收到信号才会被唤醒干活
②由于不是原子操作,当正要屏蔽信号时来了一个信号,屏蔽信号的这个操作会丢失信号信息,所以才引入这个操作取代sigrocmask,变成原子操作,不能被打断
③通过这个函数将前面已经清空信号的屏蔽集装载,因为是空集,不阻塞任何信号,
④此时,一旦收到信号,便恢复原先的信号屏蔽【我们原来的mask在上边设置的,阻塞了多达10个信号,从而保证我下边的执行流程不会再次被其他信号截断】
⑤调用该信号对应的信号处理函数就是下面的printf,信号处理函数返回后,sigsuspend返回,使程序流程继续往下走
⑥sigsuspend只使用于master进程,master进程是管理进程,管理进程只需要信号驱动就行
不适用于worker进程,不仅仅是接受信号,还要干活的,除了信号还要接受其他很多的东西来触发的

(3)日志输出重要信息弹

(3.1)换行回车进一步示意
\r:回车符,把打印或【输出】信息的位置定位到本行开头
\n:换行符,把输出为止移动到下一行 
一般把光标移动到下一行的开头,/r/n
a)比如windows下,每行结尾 \r\n
b)类Unix,每行结尾就只有\n 
c)Mac苹果系统,每行结尾只有\r
结论:统一用\n就行了
(3.2)printf()函数不加\n无法及时输出的解释
①printf末尾不加\n就无法及时的将信息显示到屏幕,缓冲区满了之后才全部打印出来
②ANSI C中定义\n认为是行刷新标记
③或者:fflush(stdout);
④或者:setvbuf(stdout,NULL,_IONBF,0); //这个函数. 直接将printf缓冲区禁止, printf就直接输出了。
⑥printf属于标准的I/O函数

(4)write()函数思考:不会引起写混乱

①多个进程同时写 一个日志文件,我们看到输出结果并不混乱,是有序的;我们的日志代码应对多进程往日志文件中写时没有问题;
②a)多个进程写一个文件(不是父子关系),可能会出现数据覆盖,混乱等情况
③解决办法:多个进程无父子关系
ngx_log.fd = open((const char *)plogname,O_WRONLY|O_APPEND|O_CREAT,0644);  

O_APPEND这个标记能够保证多个进程操作同一个文件时不会相互覆盖;
④c)内核wirte()写入时是原子操作;
⑤d)父进程fork()子进程是亲缘关系。是会共享文件表项,而且在创建子线程之前就已经提前打开了日志
⑥e)write()调用返回时,内核已经将应用程序缓冲区所提供的数据放到了内核缓冲区,但是无法保证数据已经写出到其预定的目的地【磁盘 】;
在这里插入图片描述
黄色到粉色非常快,平衡两者的读写速度,攒一堆写一次,但是断电会导致内核空间读到磁盘的数据就会丢失
(4.1)掉电导致write()的数据丢失破解法
①a)直接I/O:直接访问物理磁盘:在open的函数中加O_DIRECT:作用是绕过内核缓冲区,直接写到磁盘,但是效率很慢。用posix_memalign
②b)open文件时用O_SYNC选项:同步选项【把数据直接同步到磁盘】,只针对write函数有效,使每次write()操作等待物理I/O操作的完成;
具体说:就是将写入内核缓冲区的数据立即写入磁盘,将掉电等问题造成的损失减到最小,但是每次写磁盘数据,务必要大块大块写,一般都512-4k 4k的写;不要每次只写几个字节,否则会被抽死;************
③c)缓存同步:尽量保证缓存数据和写道磁盘上的数据一致;
有三个参数:
sync(void):将所有修改过的块缓冲区排入写队列;然后返回,并不等待实际写磁盘操作结束,数据是否写入磁盘并没有保证(可能要丢)
fsync(int fd):将fd对应的文件的块缓冲区立即写入磁盘,并等待实际写磁盘操作结束返回;***************(不注重文件属性)
fdatasync(int fd):类似于fsync,但只影响文件的数据部分。而fsync不一样,fsync除数据外,还会同步更新文件属性;(注重文件的属性)
使用例子:
write(4k),1000次之后,一直到把这个write完整[假设整个文件4M]。
fsync(fd) ,1次fsync [多次write,每次write建议都4k,然后调用一次fsync(),这才是用fsync()的正确用法
]

(5)标准IO库

//fopen,fclose
//fread,fwrite
//fflush
//fseek
//fgetc,getc,getchar
//fputc,put,putchar
//fgets,gets
//printf,fprintf,sprintf
//scanf,fscan,sscanf
在这里插入图片描述

//fwrite和write有啥区别;
//fwrite()是标准I/O库一般在stdio.h文件,仍在.h库里提供的缓冲区做中间缓冲区
//write():系统调用;

//有一句话:所有系统调用都是原子性的

标签:4.5,4.4,通讯器,worker,process,信号,进程,sa,ngx
来源: https://blog.csdn.net/weixin_43679037/article/details/116329876

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有