【nginx】开发基础知识

终端与进程的关系

1、pts(虚拟终端):xshell每连接一个窗口到虚拟机,就出现一个bash进程(黑窗口),用来解释用户输入。

whereis bash:可以查看可执行程序位置

2、终端上开启进程

用./nginx启动nginx,就可以知道bash是nginx的父进程,所以bash(终端)退出了,进程也退出了。

3、进程关系进一步分析

进程组:一个或多个进程的集合,每个进程组有唯一的ID,由系统函数来创建和加入组

会话(session):一个或多个进程组的集合

一般来说,一个bash上的所有进程都属于一个会话,这个bash进程就是session leader。

若断开xshell终端,系统会向session leader发送SIGHUP信号,session leader会将信号发给session里所有进程,最后在发给自己,这也解释了为什么关闭终端,nginx进程也停止了。

4、strace工具:跟踪程序执行时系统调用以及受到的信号(附着)

sudo strace -e trace=signal -p 进程号

5、终端关闭时如何让进程不退出

方法一:nginx进程拦截SIGHUP信号,告诉OS不要动

(1)nohup ./nginx 启动nginx,忽略SIGHUP信号,而且屏幕输出重定位到当前目录的nohup.out中;

(2)代码中加入如下内容以忽略SIGHUP信号。

signal(SIGHUP,SIG_IGN);

此时关掉终端,父死子活,nginx的PPID为1,TT为?,变为孤儿进程!

方法二:nginx进程和bash进程不在同一个session(其实就是创建了一个守护进程!)

(1)直接 setsid ./nginx 启动nginx;

(2)代码中如下:

#include <stdio.h>
#include <unistd.h>

int main(int argc, char *const *argv)
{       
    pid_t pid;    
    pid = fork(); 
    if(pid < 0)
    {
        printf("fork()进程出错!\n");
    }
    else if(pid == 0)
    {
        printf("子进程开始执行!\n");
        setsid(); //新建立一个不同的session,但是进程组组长调用setsid()是无效的
        for(;;)
        {
            sleep(1); //休息1秒
            printf("子进程休息1秒\n");
        }
        return 0;
    }
    else
    {
        //父进程会走到这里
        for(;;)
        {
            sleep(1); //休息1秒
            printf("父进程休息1秒\n");
        }
        return 0;
    }
    return 0;
}

如下图可知,该nginx进程不隶属于任何终端,SID也与bash的不同,所以关闭bash不会往此进程发SIGHUP信号。

6、后台运行:在后面加 &,./nginx &

后台运行能正常操作,如ls,cd等都可以显示信息,所以ctrl+c也是不能终止进程的,只能fg切换到前台在ctrl+c,当然前台ls,cd等命令是没有效果的。

关闭终端,进程停止,这不取决于进程在前台还是后台运行。

信号

1、基本概念

进程间常用的通信手段,如kill掉一个worker进程,master进程就会立即启动一个新的worker进程,信号用来通知某一个进程发生了某个事情(突发事情),所以进程不知道什么时候收到信号,也就是说信号是异步发生的,也被称为软件中断。

【信号如何产生】

a)某个进程发给另一个进程或发给自己

b)由内核(操作系统)发送给某个进程(ctrl+c 或 kill 或 内存访问异常 或 除以0)

信号名字:以SIG开头,如SIGHUP(终端断开信号),也就是一些数字(正整数常量,系统头文件中的宏)。

2、kill

kill其实是给进程发信号,能发多种信号,而不只是杀死进程的意思!

单纯的kill,其实是给进程发送了SIGTERM信号(终止信号),也就是kill -15 PID。

【常用数字】(很多信号的缺省动作都是杀掉进程)

1 SIGHUP

2 SIGINT(类似ctrl+c)

9 无视代码,直接kill

18 SIGCONT 使暂停的进程继续(运行在后台)

19 SIGSTOP 停止进程但后台还在

20 SIGSTP 终端停止但后台还在(类似ctrl+z)

查看进程状态:

ps -aux | grep -E 'bash|PID|nginx'

3、某个信号出现时,3中方式处理:

1)执行系统默认动作(绝大多数信号是杀进程);

2)忽略此信号(无法忽略SIGKILL和SIGSTOP,也就是-9和-19);

3)捕捉该信号(加入处理信号的函数)

Linux体系结构与信号编程初步

1、Unix/Linux操作系统体系结构

类unix操作系统体系结构分为2个状态:用户态和内核态

                 

a)os(内核):控制硬件资源,提供应用程序运行环境

写的程序不是运行在用户态就是内核态,一般在用户态;当程序要执行特殊代码时,会自动切换到内核态(无需人为介入)。

b)系统调用:就是一些系统函数

c)shell:bash(borne again shell)是shell的一种,linux上默认采用bash这种shell,bash是一个可执行程序,充当命令解释器的作用,也就是把用户输入的命令翻译给os。

whereis bash,/bin/bash可以在bash里运行一个bash,exit可以退出当前bash!

d)用户态和内核态的切换(根据需要自动切换)

用户态权限小,内核态权限大!

【为什么区分?】

①一般情况下运行在用户态,权限小,不至于危害到其他部分;

②危害部分操作系统会进行统一管理,系统提供的这些接口就是为了减少有限资源的访问和使用上的冲突。

【什么时候切换到内核态?】

①系统调用:如malloc(),printf();

②异常事件:比如来了个信号,在内核态中调用信号处理函数;

③外围设备中断:导致程序处理流程从用户态跳到内核态。

2、signal函数范例(捕捉信号)

if(signal(SIGUSR1,sig_usr) == SIG_ERR)  
//系统函数,参数1:是个信号,参数2:是个函数指针,代表一个针对该信号的捕捉处理函数
{
    printf("无法捕捉SIGUSR1信号!\n");
}

这样当kill -USR1 pid时,该进程会调用sig_usr这个信号处理函数:

void sig_usr(int signo)
{     
    if(signo == SIGUSR1)
    {
        printf("收到了SIGUSR1信号!\n");
    }
    else
    {
        printf("收到了未捕捉的信号%d!\n",signo);
    }
}

进程收到信号,会被内核注意到,具体流程如下:

【问题】如果有一个全局变量,在main中和在信号处理函数中都调用,此时来了信号,先去执行了信号处理函数而改变了此值,就会影响该值在main中的计算结果?

可重入函数:所谓的可重入函数,就是我们在信号处理函数中调用它是安全的。

不可重入函数如:malloc、printf、给全局变量赋值的函数等。

在写信号处理函数的时候,要注意的事项:

a)在信号处理函数中,尽量使用简单的语句做简单的事情,尽量不要调用系统函数以免引起麻烦;

b)如果必须要在信号处理函数中调用一些系统函数,那么要保证在信号处理函数中调用的系统函数一定要是可重入的(有个表);

c)如果必须要在信号处理函数中调用那些可能修改errno值(出现一些错误系统的返回值)的可重入的系统函数,那么就得事先备份errno值,从信号处理函数返回之前,将errno值恢复。

信号处理函数中一定一定一定要用可重入函数!

signal因为兼容性和可靠性等一些历史问题,不建议使用,用sigaction()函数代替!

信号编程进阶、sigprocmask实例

1、信号集

【问题】当一个信号处理函数运行时,系统会屏蔽此段时间内其他的信号,但必须记住,排队!

信号集:装60多种信号来或者没来的状态,1表示来了,0表示没来。

0000000000,0000000000,0000000000,00,0000000000,0000000000,0000000000,00 (64个二进制位)

linux中用sigset_t结构类型来表示。

2、信号相关函数(一个进程对应一个信号集)

a)sigemtpyset():把信号集中的所有信号都清0,表示这60多个信号都没有来,00000000000000000000000000.....

b)sigfillset():把信号集中的所有信号都设置为1,跟sigemptyset()正好相反,11111111111111111111111111.....

c)用sigaddset()和sigdelset()就可以往信号集中增加信号,或者从信号集中删除特定信号;

d)sigprocmask,sigmember

一个进程,里边会有一个信号集,用来记录当前屏蔽(阻塞)了哪些信号,sigprocmask就是用来绑定某个信号集与进程的。

如果我们把这个信号集中的某个信号位设置为1,就表示屏蔽了同类信号,此时再来个同类信号,那么同类信号会被屏蔽,不能传递给进程;如果这个信号集中有很多个信号位都被设置为1,那么所有这些被设置为1的信号都是属于当前被阻塞的而不能传递到该进程的信号。

sigprocmask()函数就能够设置该进程所对应的信号集中的内容,注意要先注册信号处理函数再设置相关的信号集。

demo关键代码:

sigset_t newmask,oldmask; //信号集,新的信号集,原有的信号集
if(signal(SIGQUIT,sig_quit) == SIG_ERR)  //注册信号对应的信号处理函数,"ctrl+\" 
{        
    printf("无法捕捉SIGQUIT信号!\n");
    exit(1);
}
//newmask信号集中所有信号都清0(表示这些信号都没有来)
sigemptyset(&newmask);
//设置newmask信号集中的SIGQUIT信号位为1,再来SIGQUIT信号时,进程就收不到
sigaddset(&newmask,SIGQUIT); 
//sigprocmask():设置该进程所对应的信号集
//第一个参数和第二个参数取并集作为当前进程的信号集,因为SIG_BLOCK为全0,
//所以其实就是用newmask作为当前进程的信号集,
//第三个参数用来保存之前的信号集,故oldmask为全0
if(sigprocmask(SIG_BLOCK,&newmask,&oldmask) < 0)
{
    printf("sigprocmask(SIG_BLOCK)失败!\n");
    exit(1);
}
//测试一个指定的信号位是否被置位(为1),测试的是newmask的SIGQUIT位,此处应该是屏蔽了
if(sigismember(&newmask,SIGQUIT))
{
    printf("SIGQUIT信号被屏蔽了!\n");
}
...
//第一个参数用了SIGSETMASK表明设置进程新的信号屏蔽字为第二个参数指向的信号集,第三个参数没用
if(sigprocmask(SIG_SETMASK,&oldmask,NULL) < 0) 
{
    printf("sigprocmask(SIG_SETMASK)失败!\n");
    exit(1);
}

如果在屏蔽期间发了数个“ctrl+\”信号,则在打开屏蔽后进程会合n为1收到一个“ctrl+\”信号。

sleep()函数能够被打断,来了某个信号会使sleep()提前结束,sleep会返回一个值,这个值就是未睡够的时间!

如果在信号处理函数中加入这几行,则第二次信号到来时,会默认为缺省处理(终止进程),直接quit终止进程(不“再见”)!

if(signal(SIGQUIT,SIG_DFL) == SIG_ERR)
{
    printf("无法为SIGQUIT信号设置缺省处理(终止进程)!\n");
    exit(1);
}

以后(商业代码)sigaction要取代signal!

fork

1、fork函数简单认识

进程:一个可执行程序,执行起来就是一个进程,再执行起来一次又是一个进程(多个进程可以共享同一个可执行文件)。

其他解释:程序执行的一个实例,用fork创建一个子进程,相当于创建含有相同一段的两条执行通路。

图示如下:

     

fork()之后,是父进程fork()之后的代码先执行还是子进程fork()之后的代码先执行是不一定的,这个跟内核调度算法有关!

【问题】kill子进程,观察父进程收到什么信号?

【回答】用strace,父进程收到来自子进程的SIGCHLD信号,子进程随后变为僵尸进程,STAT为Z+。僵尸进程占用资源的,至少占用pid号,系统中是有限制的,所以开发者要杜绝僵尸进程的存在!

2、僵尸进程的产生和解决

1)产生

在Unix系统中,子死(可能是被kill也可能只是结束了)父活,但父没有调用(wait/waitpid)函数来进行额外的处置,子进程就会变成一个僵尸进程;

※ 僵尸进程已经被终止,不干活了,但还没有被内核丢弃,因为内核认为父进程可能还用子进程的一些信息。

2)解决

a)重启电脑;

b)手工地把僵尸进程的父进程kill掉,僵尸进程就会自动消失;

c)SIGCHLD信号:一个进程被终止或者停止时,这个信号会被子进程发送给父进程;所以,对于源码中有fork()行为的进程,我们应该拦截并处理SIGCHLD信号。

pid_t pid = waitpid(-1,&status,WNOHANG);
//第一个参数为-1,表示等待任何子进程
//第二个参数:保存子进程的状态信息
//第三个参数:提供额外选项,WNOHANG表示不要阻塞,让这个waitpid()立即返回
if(pid == 0)
//子进程没结束,会立即返回这个数字,但这里应该不是这个数字                        
    return;
if(pid == -1)
//这表示这个waitpid调用有错误,有错误也立即返回
    return;
//走到这里,表示成功,直接return
return;   

3、fork函数的进一步认识(写时复制机制

fork()产生新进程的速度非常快,fork()产生的新进程并不复制原进程的内存空间,而是和原进程(父进程)一起共享一个内存空间,但这个内存空间的特性是“写时复制”,也就是说:原来的进程和fork()出来的子进程可以同时、自由的读取内存,但如果子进程(父进程)对内存进行修改的话,那么这个内存就会复制一份给该其他进程单独使用,以免影响到共享这个内存空间的其他进程使用。

4、完善fork代码

fork()回返回两次:父进程中返回一次,子进程中返回一次。而且,fork()在父进程中返回的值和在子进程中返回的值是不同的,子进程的fork()返回值是0,父进程的fork()返回值是新建立的子进程的ID(所以返回pid是大于0的)。

如果有一个全局变量g_mygbltest,而且某个进程中对该值有改变动作,会导致父子进程内存分开(写时复制机制),所以即使是全局变量,两个g_mygbltest的值也是不同的(因为是在不同的进程中)。

5、一道逻辑题

连续fork两次,创建4个进程;如下操作,产生7个进程,注意短路求值! 

((fork() && fork()) || (fork() && fork()));

6、fork失败的可能性

也就是超过这些量fork进程会失败!

a)系统中进程太多:缺省情况最大的pid为32767;

b)每个用户有个允许开启的进程总数:sysconf(_SC_CHILD_MAX)查看,大约7788。

守护进程

1、回顾

ps -eo pid,ppid,sid,tty,pgrp,comm,stat,cmd | grep -E 'bash|PID|nginx'

1)进程有对应的终端,如果终端退出,那么对应的进程也就消失了,它的父进程是一个bash;

2)终端被占住了,输入各种命令这个终端都没有反应。

2、基本概念

一种长期运行的进程,这种进程在后台运行,并且不跟任何的控制终端关联

【基本特点】

a)生存期长(不是必须,但一般这样做),一般是操作系统启动的时候他就启动,操作系统关闭的时候它才关闭;

b)守护进程跟终端无关联,也就是说他们没有控制终端,所以你控制终端退出,也不会导致守护进程退出;

c)守护进程是在后台运行,不会占着终端,终端可以执行其他命令。

linux操作系统本身是有很多的守护进程在默默的运行,维持着系统的日常活动。大概30-50个。

ps -efj    //e所有进程,f完整格式,j与作业或任务相关

a)ppid = 0:内核进程,跟随系统启动而启动,声明周期贯穿整个系统;

b)CMD列名字带[ ]这种,叫内核守护进程;

c)老祖init:也是系统守护进程,它负责启动各运行层次特定的系统服务;所以很多进程的PPID是init,而且这个init也负责收养孤儿进程

d)CMD列中名字不带[ ]的普通守护进程(用户级守护进程)

【共同点总结】:

a)大多数守护进程都是以超级用户特权运行的

b)守护进程没有控制终端,TT这列显示?

c)内核守护进程以无控制终端方式启动

d)普通守护进程可能是守护进程调用了setsid的结果(无控制端)

3、守护进程编写规则

(1)调用umask(0):umask是个函数,用来限制(屏蔽)一些文件权限的。

(2)fork()一个子进程(脱离终端)出来,然后父进程退出(把终端空出来,不让终端卡住),固定套路。

fork()的目的是想成功调用setsid()来建立新会话,目的是子进程有单独的sid(因为进程组组长没法setsid),这样,子进程成为了一个新进程组的组长进程,子进程也不关联任何终端了。

4、其他重要概念

1)文件描述符:正数,用来标识一个文件。

当你打开一个存在的文件或者创建一个新文件,操作系统都会返回这个文件描述符(其实就是代表这个文件的),后续对这个文件的操作的一些函数,都会用到这个文件描述符作为参数;

linux中三个特殊的文件描述符,数字分别为0,1,2

0:标准输入【键盘】,对应的符号常量叫STDIN_FILENO

1:标准输出【屏幕】,对应的符号常量叫STDOUT_FILENO

2:标准错误【屏幕】,对应的符号常量叫STDERR_FILENO

类Unix操作系统,默认从STDIN_FILENO读数据,向STDOUT_FILENO来写数据,向STDERR_FILENO来写错误。一切皆文件,把标准输入,标准输出,标准错误都看成文件。同时,程序一旦运行起来,这三个文件描述符0,1,2会被自动打开(自动指向对应的设备)。

文件描述符虽然是数字,但如果我们把文件描述符直接理解成指针(指针里边保存的是地址——地址说白了也是个数字)。

write(STDOUT_FILENO,"aaaabbb",6);    //屏幕上输出aaaabb

2)输入输出重定向

输出重定向:我标准输出文件描述符,不指向屏幕了,假如我指向(重定向)一个文件;

输出重定向,在命令行中用 > 可将本来显示在屏幕上的内容放入myoutfile文件:

ls -la > myoutfile

输入重定向,相当于输入的为myinfile的内容:

cat < myinfile

合用,把myinfile里的内容当作输入通过cat输出,但是将本来显示在屏幕上的内容放入myoutfile:

cat < myinfile > myoutfile

3)空设备

/dev/null:是一个特殊的设备文件,它丢弃一切写入其中的数据(象黑洞一样)。

【注意】守护进程虽然可以通过终端启动,但是和终端不挂钩。守护进程是在后台运行,它不应该从键盘上接收任何东西,也不应该把输出结果打印到屏幕或者终端上来。所以,一般按照江湖规矩,我们要把守护进程的标准输入和标准输出重定向到空设备(黑洞),从而确保守护进程不从键盘接收任何东西,也不把输出结果打印到屏幕。

【核心代码demo】

int fd;
fd = open("/dev/null",O_RDWR);    //打开空设备
dup2(fd,STDIN_FILENO);    //复制文件描述符,像个指针赋值,把第一个参数指向的内容赋给了第二个参数
dup2(fd,STDOUT_FILENO);    //同上
if(fd > STDERR_FILENO)    //012都被占,fd至少是个3
    close(fd);    //等价于fd = null;

dup2图示如下:(dup2还可以先关闭原来指向的文件描述符,所以商业代码中尽量用dup2) 

守护进程可以用命令启动,如果想开机启动,则需要借助系统初始化脚本来启动。

5、守护进程不会受到的信号

1)SIGHUP信号:守护进程不会收到来自内核的 SIGHUP 信号,潜台词就是如果守护进程收到了 SIGHUP 信号,那么肯定是另外的进程发给来的(SIGHUP是Session Leader发给其他进程的,守护进程不关联终端,所以不会受到)。

很多守护进程把这个信号作为通知信号,表示配置文件已经发生改动,守护进程应该重新读入其配置文件。

如在nginx中,就是用SIGHUP信号来通知会话首进程(master)配置文件有变动,需要重启4个worker进程。

sudo ./nginx -s reload    //执行这行后,重启4个worker进程
等价于
sudo kill -1 master进程号

2)SIGINT、SIGWINCH信号

守护进程不会收到来自内核的 SIGINT(ctrl+c),SIGWINCH(终端窗口大小改变)信号,所以可以拿来自己用。

6、守护进程和后台进程的区别

1)守护进程和终端不挂钩,后台进程能往终端上输出东西(如 printf 照样打印,是和终端挂钩的);

2)守护进程关闭终端时不受影响,后台进程会随着终端的退出而退出。

猜你喜欢

转载自blog.csdn.net/u012836896/article/details/88950883