【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)守护进程关闭终端时不受影响,后台进程会随着终端的退出而退出。