一:终端和进程的关系
小工具:ps,kill,strace介绍:
ps:用来显示进程信息的命令;
kill:用于往进程发送信号,比如命令kill -2 进程id,发送SIGINT信号,kill -1 进程id,发送SIGHUP信号;
strace:用来显示进程收到的信号;
通过小例子达到的目的:利用fork产生一个子进程;然后通过ps显示进程信息,通过strace跟踪kill掉子进程后父进程收到SIGCHLD信号;
/*
利用fork产生一个子进程
*/
#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");
while(true)
sleep(1); //休息1秒
}
else
{//父进程
while(true)
sleep(1); //休息1秒
}
return 0;
}
//执行上述代码后,运行ps显示结果
//pid:进程号
//pid:父进程号
//sid:会话号,bash上运行的进程属于一个会话,bash进程为会话的leader;
//tty:bash终端标识
//pgrp:进程组号,一般父进程为组长ID;
//comm:comm执行的命令
root@epc:~# ps -eo pid,ppid,sid,tty,pgrp,comm | grep -E 'bash|PID|test'
PID PPID SID TT PGRP COMMAND
1733 1256 1256 tty1 1733 bash
1831 1812 1831 pts/0 1831 bash
3533 3512 3533 pts/1 3533 bash
3585 1831 1831 pts/0 3585 test
3586 3585 1831 pts/0 3585 test
#跟踪父进程
root@epc:~# strace -e trace=signal -p 3585
strace: Process 3585 attached
#kill掉子进程
root@epc:~# kill 3586
root@epc:~#
#父进程收到了SIGCHLD信号
root@epc:~# strace -e trace=signal -p 3585
strace: Process 3585 attached
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_KILLED, si_pid=3586, si_uid=0, si_status=SIGTERM, si_utime=0, si_stime=0} ---
#再用ps查看进程,发现子进程处于僵尸进程状态
root@epc:~# ps -eo pid,ppid,sid,tty,pgrp,comm | grep -E 'PID|test'
PID PPID SID TT PGRP COMMAND
3585 1831 1831 pts/0 3585 test
3586 3585 1831 pts/0 3585 test <defunct>
二:终端关闭时如何让进程不退出?
解决:
1.程序拦截SIGHUP信号:用signal函数,但要注意忽略此信号不包括SIGKILL和SIGSTOP信号,是一定能够把这个进程杀掉的;kill -9 进程id可以触发此信号。
2.进程和bash进程不在同一个session:子进程中setsid();
/*
程序拦截SIGHUP信号;
*/
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>
int main(int argc, char *const *argv)
{
signal(SIGHUP, SIG_IGN); //SIG_IGN:要求系统忽略这个信号
pid_t pid;
pid = fork(); //系统函数,用来创建新进程
if(pid < 0)
printf("fork()进程出错!\n");
else if(pid == 0)
{ //子进程
printf("子进程开始执行!\n");
while(true)
sleep(1); //休息1秒
}
else
{//父进程
while(true)
sleep(1); //休息1秒
}
return 0;
}
关闭终端前PS:
root@epc:~# ps -eo pid,ppid,sid,tty,pgrp,comm |grep -E 'bash|PID|test'
PID PPID SID TT PGRP COMMAND
1733 1256 1256 tty1 1733 bash
3760 3741 3760 pts/0 3760 bash
3816 3796 3816 pts/3 3816 bash
3837 3816 3816 pts/3 3837 test
3838 3837 3816 pts/3 3837 test
关闭终端后PS:
root@epc:~# ps -eo pid,ppid,sid,tty,pgrp,comm |grep -E 'bash|PID|test'
PID PPID SID TT PGRP COMMAND
1733 1256 1256 tty1 1733 bash
3760 3741 3760 pts/0 3760 bash
3837 1 3816 ? 3837 test
3838 3837 3816 ? 3837 test
说明:关闭bash终端后,作为session leader会向所有session中的进程发送SIGHUP信号,默认行为为关闭进程,如果通过signal函数捕获后,进程则会有其他行为,SIG_IGN的行为就是忽略处理。
/*
设置子进程为setsid();
*/
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main(int argc, char *const *argv)
{
pid_t pid;
pid = fork(); //系统函数,用来创建新进程
if(pid < 0)
printf("fork()进程出错!\n");
else if(pid == 0)
{ //子进程
setsid();// 新建立一个不同的session,但是进程组组长使用setsid()是无效的;
printf("子进程开始执行!\n");
while(true)
sleep(1); //休息1秒
}
else
{//父进程
while(true)
sleep(1); //休息1秒
}
return 0;
}
关闭终端前PS:
root@epc:~# ps -eo pid,ppid,sid,tty,pgrp,comm | grep -E 'bash|PID|test'
PID PPID SID TT PGRP COMMAND
1733 1256 1256 tty1 1733 bash
3533 3512 3533 pts/1 3533 bash
3634 3615 3634 pts/2 3634 bash
3712 3533 3533 pts/1 3712 test
3713 3712 3713 ? 3713 test
关闭终端后PS:
root@epc:~# ps -eo pid,ppid,sid,tty,pgrp,comm | grep -E 'bash|PID|test'
PID PPID SID TT PGRP COMMAND
1733 1256 1256 tty1 1733 bash
3634 3615 3634 pts/2 3634 bash
3713 1 3713 ? 3713 test
root@epc:~#
说明:setsid后子进程不受终端影响,终端退出,不影响子进程,子进程由init进程接管;
三:后台运行 &
如果程序后面没有用&进行后台执行,那么终端就只能等这个程序完成后才能继续执行其他的操作;
在终端后使用fg命令切换到前台;
#执行./test & 后,关闭终端,进程会被init进程接管;
root@epc:~# ps -eo pid,ppid,sid,tty,pgrp,comm |grep -E 'bash|PID|test'
PID PPID SID TT PGRP COMMAND
1733 1256 1256 tty1 1733 bash
3760 3741 3760 pts/0 3760 bash
3884 1 3860 ? 3884 test
3885 3884 3860 ? 3884 test
root@epc:~#
四:用户态和内核态
如上图所示,从宏观上来看,Linux操作系统的体系架构分为用户态和内核态(或者用户空间和内核)。内核从本质上看是一种软件——控制计算机的硬件资源,并提供上层应用程序运行的环境。用户态即上层应用程序的活动空间,应用程序的执行必须依托于内核提供的资源,包括CPU资源、存储资源、I/O资源等。为了使上层应用能够访问到这些资源,内核必须为上层应用提供访问的接口:即系统调用。
系统调用是操作系统的最小功能单位,这些系统调用根据不同的应用场景可以进行扩展和裁剪,现在各种版本的Unix实现都提供了不同数量的系统调用,如Linux的不同版本提供了240-260个系统调用,FreeBSD大约提供了320个(reference:UNIX环境高级编程)。我们可以把系统调用看成是一种不能再化简的操作(类似于原子操作,但是不同概念),有人把它比作一个汉字的一个“笔画”,而一个“汉字”就代表一个上层应用,我觉得这个比喻非常贴切。因此,有时候如果要实现一个完整的汉字(给某个变量分配内存空间),就必须调用很多的系统调用。如果从实现者(程序员)的角度来看,这势必会加重程序员的负担,良好的程序设计方法是:重视上层的业务逻辑操作,而尽可能避免底层复杂的实现细节。库函数正是为了将程序员从复杂的细节中解脱出来而提出的一种有效方法。它实现对系统调用的封装,将简单的业务逻辑接口呈现给用户,方便用户调用,从这个角度上看,库函数就像是组成汉字的“偏旁”。这样的一种组成方式极大增强了程序设计的灵活性,对于简单的操作,我们可以直接调用系统调用来访问资源,如“人”,对于复杂操作,我们借助于库函数来实现,如“仁”。显然,这样的库函数依据不同的标准也可以有不同的实现版本,如ISO C 标准库,POSIX标准库等。
Shell是一个特殊的应用程序,俗称命令行,本质上是一个命令解释器,它下通系统调用,上通各种应用,通常充当着一种“胶水”的角色,来连接各个小功能程序,让不同程序能够以一个清晰的接口协同工作,从而增强各个程序的功能。同时,Shell是可编程的,它可以执行符合Shell语法的文本,这样的文本称为Shell脚本,通常短短的几行Shell脚本就可以实现一个非常大的功能,原因就是这些Shell语句通常都对系统调用做了一层封装。为了方便用户和系统交互,一般,一个Shell对应一个终端,终端是一个硬件设备,呈现给用户的是一个图形化窗口。我们可以通过这个窗口输入或者输出文本。这个文本直接传递给shell进行分析解释,然后执行。
五:重入函数
在实时系统的设计中,经常会出现多个任务调用同一个函数的情况。如果有一个函数不幸被设计成为这样:不同任务调用这个函数时可能修改其他任务调用这个函数的数据,从而导致不可预料的后果。这样的函数是不安全的函数,也叫不可重入函数。
那么什么是可重入函数呢?所谓可重入是指一个可以被多个任务调用的过程,任务在调用时不必担心数据是否会出错。
也可以这样理解,重入即表示重复进入,首先它意味着这个函数可以被中断,其次意味着它除了使用自己栈上的变量以外不依赖于任何环境(包括 static),这样的函数就是purecode(纯代码)可重入,可以允许有该函数的多个副本在运行,由于它们使用的是分离的栈,所以不会互相干扰。如果确实需要访问全局变量(包括 static),一定要注意实施互斥手段。可重入函数在并行运行环境中非常重要,但是一般要为访问全局变量付出一些性能代价。
编写可重入函数时,若使用全局变量,则应通过关中断、信号量(即P、V操作)等手段对其加以保护。
说明:若对所使用的全局变量不加以保护,则此函数就不具有可重入性,即当多个进程调用此函数时,很有可能使有关全局变量变为不可知状态。
保证函数的可重入性的方法:
1)在写函数时候尽量使用局部变量(例如寄存器、堆栈中的变量);
2)对于要使用的全局变量要加以保护(如采取关中断、信号量等互斥方法),这样构成的函数就一定是一个可重入的函数。
满足下列条件的函数多数是不可重入(不安全)的:
1)函数体内使用了静态的数据结构;
2)函数体内调用了malloc() 或者 free() 函数;
3)函数体内调用了标准 I/O 函数。
常见的可重入函数:
五:信号编程进阶
signal是一个不稳定函数,应该用sigaction取代;
信号集的定义:信号集表示一组信号的来(1)或者没来(0);
一个进程,里边会有一个信号集,用来记录当前屏蔽(阻塞)了哪些信号;如果我们把这个信号集中的某个信号位设置为1,就表示屏蔽了同类信号,此时再来个同类信号,那么同类信号会被屏蔽,不能传递给进程;如果这个信号集中有很多个信号位都被设置为1,那么所有这些被设置为1的信号都是属于当前被阻塞的而不能传递到该进程的信号;
sigprocmask()函数,就能够设置该进程所对应的信号集中的内容;
#include <signal.h>
int sigprocmask( int how, const sigset_t *restrict set, sigset_t *restrict oset );
@sigprocmask:信号集函数,一个进程的信号屏蔽字规定了当前阻塞而给该进程的信号集。调用函数sigprocmask可以检测或更改其信号屏蔽字;
@int:若成功则返回0,若出错则返回-1,errno被设为EINVAL
@how:用于指定信号修改的方式,可能选择有三种;
SIG_BLOCK:将set所指向的信号集中包含的信号加到当前的信号掩码中;
SIG_UNBLOCK:将set所指向的信号集中包含的信号从当前的信号掩码中删除;
SIG_SETMASK:将set的值设定为新的进程信号掩码;
@set:为指向信号集的指针,在此专指新设的信号集,如果仅想读取现在的屏蔽值,可将其置为NULL;
@oset:也是指向信号集的指针,在此存放原来的信号集。可用来检测信号掩码中存在什么信号;
linux 是用sigset_t结构类型来表示信号集;
typedef struct{
unsigned long sig[2];
} sigset_t
#include <signal.h>
int sigemptyset(sigset_t *set);
@sigemptyset:用来将参数set信号集初始化并清空;
@int:若成功则返回0,若出错则返回-1,errno被设为EFAULT;
@set:信号集;
#include <signal.h>
int sigfillset(sigset_t * set);
@sigfillset:用来将参数set信号集初始化,然后把所有的信号加入到此信号集里即将所有的信号标志位置为1,屏蔽所有的信号;
@int:若成功则返回0,若出错则返回-1,errno被设为EFAULT;
@set:信号集;
#include <signal.h>
int sigismember(const sigset_t *set,int signum);
@sigismember:测试参数signum 代表的信号是否已加入至参数set信号集里
@int:如果信号集里已有该信号则返回1,否则返回0。如果有错误则返回-1;
@set:信号集;
@signum:信号;
//小例子
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
//信号处理函数
void sig_quit(int signo)
{
printf("收到了SIGQUIT信号!\n");
if(signal(SIGQUIT,SIG_DFL) == SIG_ERR)
{
printf("无法为SIGQUIT信号设置缺省处理(终止进程)!\n");
exit(1);
}
}
int main(int argc, char *const *argv)
{
sigset_t newmask,oldmask; //信号集,新的信号集,原有的信号集,挂起的信号集
if(signal(SIGQUIT,sig_quit) == SIG_ERR) //注册信号对应的信号处理函数,"ctrl+\"
{
printf("无法捕捉SIGQUIT信号!\n");
exit(1); //退出程序,参数是错误代码,0表示正常退出,非0表示错误,但具体什么错误,没有特别规定,这个错误代码一般也用不到,先不管他;
}
sigemptyset(&newmask); //newmask信号集中所有信号都清0(表示这些信号都没有来);
sigaddset(&newmask,SIGQUIT); //设置newmask信号集中的SIGQUIT信号位为1,说白了,再来SIGQUIT信号时,进程就收不到,设置为1就是该信号被阻塞掉呗
//sigprocmask():设置该进程所对应的信号集
if(sigprocmask(SIG_BLOCK,&newmask,&oldmask) < 0) //SIG_BLOCK:将set所指向的信号集中包含的信号加到当前的信号掩码中;
{ //newmask:一个 ”进程“ 的当前信号屏蔽字,刚开始全部都是0的;所以相当于把当前 "进程"的信号屏蔽字设置成 newmask(屏蔽了SIGQUIT);
//oldmask:进程老的(调用本sigprocmask()之前的)信号集会保存到第三个参数里,用于后续,这样后续可以恢复老的信号集给线程
printf("sigprocmask(SIG_BLOCK)失败!\n");
exit(1);
}
printf("我要开始休息10秒了--------begin--,此时我无法接收SIGQUIT信号!\n");
sleep(10); //这个期间无法收到SIGQUIT信号的;
printf("我已经休息了10秒了--------end----!\n");
if(sigismember(&newmask,SIGQUIT)) //测试一个指定的信号位是否被置位(为1),测试的是newmask
{
printf("SIGQUIT信号被屏蔽了!\n");
}
else
{
printf("SIGQUIT信号没有被屏蔽!!!!!!\n");
}
if(sigismember(&newmask,SIGHUP)) //测试另外一个指定的信号位是否被置位,测试的是newmask
{
printf("SIGHUP信号被屏蔽了!\n");
}
else
{
printf("SIGHUP信号没有被屏蔽!!!!!!\n");
}
//现在我要取消对SIGQUIT信号的屏蔽(阻塞)--把信号集还原回去
if(sigprocmask(SIG_SETMASK,&oldmask,NULL) < 0) //第一个参数用了SIGSETMASK表明设置 进程 新的信号屏蔽字为 第二个参数 指向的信号集,第三个参数没用
{
printf("sigprocmask(SIG_SETMASK)失败!\n");
exit(1);
}
printf("sigprocmask(SIG_SETMASK)成功!\n");
if(sigismember(&oldmask,SIGQUIT)) //测试一个指定的信号位是否被置位,这里测试的当然是oldmask
{
printf("SIGQUIT信号被屏蔽了!\n");
}
else
{
printf("SIGQUIT信号没有被屏蔽,您可以发送SIGQUIT信号了,我要sleep(10)秒钟!!!!!!\n");
int mysl = sleep(10);
if(mysl > 0)
{
printf("sleep还没睡够,剩余%d秒\n",mysl);
}
}
printf("再见了!\n");
return 0;
}
/*
操作步骤:
1.运行程序:
root@epc:/home/share/project/test/linux# ./test
我要开始休息10秒了--------begin--,此时我无法接收SIGQUIT信号!
我已经休息了10秒了--------end----!
SIGQUIT信号被屏蔽了!
SIGHUP信号没有被屏蔽!!!!!!
sigprocmask(SIG_SETMASK)成功!
SIGQUIT信号没有被屏蔽,您可以发送SIGQUIT信号了,我要sleep(10)秒钟!!!!!!
2.等待程序运行到“SIGQUIT信号没有被屏蔽,您可以发送SIGQUIT信号了,我要sleep(10)秒钟!!!!!!”时,执行kill命令;
root@epc:~# ps -ef|grep test
root 2037 1917 0 19:02 pts/0 00:00:00 ./test
root@epc:~# kill -s SIGQUIT 2037
3.执行完kill命令后,程序往下执行结果为:
收到了SIGQUIT信号!
sleep还没睡够,剩余6秒
再见了!
root@epc:/home/share/project/test/linux#
*/
六:信号知识补充
1.信号触发时,如果回调函数在执行中,进程多次收到相同信号,那么进程会在执行完回调函数后,只会再触发一次信号;
2.信号触发时,如果回调函数在执行中,进程收到了其他信号,那么进程会先处理其他信号,等处理完后,再处理自己;
//验证代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
//信号处理函数
void sig_usr(int signo)
{
if(signo == SIGUSR1)
{
printf("收到了SIGUSR1信号,我休息10秒......!\n");
sleep(10);
printf("收到了SIGUSR1信号,我休息10秒完毕,苏醒了......!\n");
}
else if(signo == SIGUSR2)
{
printf("收到了SIGUSR2信号,我休息10秒......!\n");
sleep(10);
printf("收到了SIGUSR2信号,我休息10秒完毕,苏醒了......!\n");
}
else
{
printf("收到了未捕捉的信号%d!\n",signo);
}
}
int main(int argc, char *const *argv)
{
if(signal(SIGUSR1,sig_usr) == SIG_ERR) //系统函数,参数1:是个信号,参数2:是个函数指针,代表一个针对该信号的捕捉处理函数
{
printf("无法捕捉SIGUSR1信号!\n");
}
if(signal(SIGUSR2,sig_usr) == SIG_ERR)
{
printf("无法捕捉SIGUSR2信号!\n");
}
for(;;)
{
sleep(1); //休息1秒
printf("休息1秒~~~~!\n");
}
printf("再见!\n");
return 0;
}
七:fork函数
a)缺省情况,最大的pid:32767
b)每个用户有个允许开启的进程总数:7788
#include<unistd.h>/*#包含<unistd.h>*/
#include<sys/types.h>/*#包含<sys/types.h>*/
pid_t fork( void);
@fork:fork系统调用用于创建一个新进程,称为子进程,它与进程(称为系统调用fork的进程)同时运行,此进程称为父进程;
@pid_t:
负值:创建子进程失败。
零:返回到新创建的子进程。
正值:返回父进程或调电者。该值包含新创建的子进程的进程ID。
//fork
#include <stdio.h>
#include <stdlib.h> //malloc,exit
#include <unistd.h> //fork
#include <signal.h>
int main(int argc, char *const *argv)
{
((fork() && fork()) || (fork() && fork()));
printf("每个实际用户ID的最大进程数=%ld\n",sysconf(_SC_CHILD_MAX));
for(;;)
{
sleep(1); //休息1秒
printf("休息1秒,进程id=%d!\n",getpid());
}
printf("再见了!\n");
return 0;
}
结果:
root@ubuntu:~# ps -ef|grep test
root 1140 1000 0 15:14 pts/1 00:00:00 ./test
root 1141 1140 0 15:14 pts/1 00:00:00 ./test
root 1142 1141 0 15:14 pts/1 00:00:00 ./test
root 1143 1141 0 15:14 pts/1 00:00:00 ./test
root 1144 1140 0 15:14 pts/1 00:00:00 ./test
root 1145 1144 0 15:14 pts/1 00:00:00 ./test
root 1146 1144 0 15:14 pts/1 00:00:00 ./test
八:守护进程
守护进程(daemon)是一类在后台运行的特殊进程,用于执行特定的系统任务。很多守护进程在系统引导的时候启动,并且一直运行直到系统关闭。另一些只在需要的时候才启动,完成任务后就自动结束
守护进程和后台进程的区别:
(1)守护进程和终端不挂钩;后台进程能往终端上输出东西(和终端挂钩);
(2)守护进程关闭终端时不受影响,守护进程会随着终端的退出而退出;
守护进程通用编写套路:
(1)调用umask(0)
umask是个函数,用来限制(屏蔽)一些文件权限的。
(2)fork()一个子进程(脱离终端)出来,然后父进程退出( 把终端空出来,不让终端卡住),固定套路。
fork()的目的是想成功调用setsid()来建立新会话,目的是子进程有单独的sid,而且子进程也成为了一个新进程组的组长进程。同时,子进程不关联任何终端了。
屏蔽标准输入输出:
守护进程是在后台运行,它不应该从键盘上接收任何东西,也不应该把输出结果打印到屏幕或者终端上来
所以,我们要把守护进程的 标准输入,标准输出,重定向到 空设备(黑洞),从而确保守护进程不从键盘接收任何东西,也不把输出结果打印到屏幕。
守护进程不会收到的信号:
(1)SIGHUP信号
守护进程不会收到来自内核的 SIGHUP 信号; 潜台词就是 如果守护进程收到了 SIGHUP信号,那么肯定是另外的进程发给你的。很多守护进程把这个信号作为通知信号,表示配置文件已经发生改动,守护进程应该重新读入其配置文件。
(2)SIGINT、SIGWINCH信号
守护进程不会收到来自内核的SIGINT(ctrl+C),SIGWINCH(终端窗口大小改变) 信号;
//守护进程的固定套路写法
#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;
}