转载学习:
所有的信号都有自己的名字。这些名字都以“SIG”开头,只是后面有所不同。开发人员可以通过这些名字了解到系统中发生了什么事。当信号出现时,开发人员可以要求系统进行以下三种操作:
◆ 忽略信号。大多数信号都是采取这种方式进行处理的,这里就采用了这种用法。但值得注意的是对SIGKILL和SIGSTOP信号不能做忽略处理。
◆ 捕捉信号。最常见的情况就是,如果捕捉到SIGCHID信号,则表示子进程已经终止。然后可在此信号的捕捉函数中调用waitpid()函数取得该子进程的进程ID和它的终止状态。另外,如果进程创建了临时文件,那么就要为进程终止信号SIGTERM编写一个信号捕捉函数来清除这些临时文件。
◆ 执行系统的默认动作。对绝大多数信号而言,系统的默认动作都是终止该进程。
对这些有关终端的信号,一般采用忽略处理,从而保障了终端免受干扰。
这类信号分别是,SIGTTOU(表示后台进程写控制终端)、SIGTTIN(表示后台进程读控制终端)、SIGTSTP(表示终端挂起)和SIGHUP(进程组长退出时向所有会议成员发出的)。
2.将程序进入后台执行。由于守护进程最终脱离控制终端,到后台去运行。方法是在进程中调用fork使父进程终止,让Daemon在子进程中后台执行。这就是常说的“脱壳”。子进程继续函数fork()的定义如下:
其中/dev/tty是一个流设备,也是终端映射,调用close()函数将终端关闭。
4.禁止进程重新打开控制终端。进程已经成为无终端的会话组长,但它可以重新申请打开一个控制终端。开发人员可以通过不再让进程成为会话组长的方式来禁止进程重新打开控制终端,需要再次调用fork函数。
上面的程序代码表示结束第一子进程,第二子进程继续(第二子进程不再是会话组长)。
5. 关闭打开的文件描述符,并重定向标准输入、标准输出和标准错误输出的文件描述符。进程从创建它的父进程那里继承了打开的文件描述符。如果不关闭,将会浪费系统资源,引起无法预料的错误。关闭三者的代码如下:
但标准输入、标准输出和标准错误输出的重定向是可选的。也许有的程序想保留标准输入(0)、标准输出(1)和标准错误输出(2),那么循环应绕过这三者。代码如下:
for (fd =3, fdtablesize = getdtablesize(); fd < fdtablesize; fd++) close(fd);
有的程序有些特殊的需求,还需要将这三者重新定向。示例如下:
6.改变工作目录到根目录或特定目录进程活动时,其工作目录所在的文件系统不能卸下。
一般需要将工作目录改变到根目录或特定目录,注意用户对此目录需要有读写权。防止超级用户卸载设备时系统报告设备忙。
7.处理SIGCHLD信号。SIGCHLD信号是子进程结束时,向内核发送的信号。
如果父进程不等待子进程结束,子进程将成为僵尸进程(zombie)从而占用系统资源。因此需要对SIGCHLD信号做出处理,回收僵尸进程的资源,避免造成不必要的资源浪费。可以用如下语句:
signal(SIGCHLD,(void *)reap_status);
捕捉信号SIGCHLD,用下面的函数进行处理:
8.在Linux/Unix下有个syslogd的守护进程,向用户提供了syslog()系统调用。任何程序都可以通过syslog记录事件。
由于syslog非常好用和易配置,所以很多程序都使用syslog来发送它们的记录信息。一般守护进程也使用syslog向系统输出信息。syslog有三个函数,一般只需要用syslog(...)函数,openlog()/closelog()可有可无。syslog()在shslog.h定义如下: #include <syslog.h> void syslog(int priority,char *format,...);
其中参数priority指明了进程要写入信息的等级和用途。第二个参数是一个格式串,指定了记录输出的格式。在这个串的最后需要指定一个%m,对应errno错误码。
应用范例
linux上编写守护进程的例程
摘自《开放系统世界》2004年第5期郭吉平、任莲的文章“亲自动手编写守护进程”。
/*郭吉平、任莲 亲自动手编写守护进程*/
#include
#include
#include
void main(int argc, char ** argv){
time_t now;
int childpid, fd, fdtablesize;
int error, in, out;
/*忽略终端 I/O信号,STOP信号*/
signal(SIGTTOU,SIG_IGN);
signal(SIGTTIN,SIG_IGN);
signal(SIGTSTP,SIG_IGN);
signal(SIGHUP,SIG_IGN);
/*父进程退出,程序进入后台运行*/
if( fork()!=0 ) exit(1);
if( setsid()<0 ) exit(1); /*创建一个新的会议组*/
/*子进程退出,孙进程没有控制终端了*/
if( fork()!=0 ) exit(1);
if( chdir("/tmp")==-1 )exit(1);
/*关闭打开的文件描述符,包括标准输入、标准输出和标准错误输出*/
for( fd=0, fdtablesize=getdtablesize(); fd< fdtablesize;fd++) close(fd);
umask(0);/*重设文件创建掩模*/
signal(SIGCHLD,SIG_IGN);/*忽略SIGCHLD信号*/
/*打开log系统*/
syslog(LOG_USER|LOG_INFO,"守护进程测试!/n");
while(1){
time(&now);
syslog(LOG_USER|LOG_INFO,"当前时间:/t%s/t/t/n",ctime(&now));
sleep(6);
}
}
守护进程在Linux/Unix系统中有着广泛的应用。有时,开发人员也想把自己的程序变成守护进程。在创建一个守护进程的时候,要接触到子进程、进程组、会晤期、信号机制、文件、目录和控制终端等多个概念。因此守护进程还是比较复杂的,在这里详细地讨论Linux/Unix的守护进程的编写,总结出八条经验,并给出应用范例。
编程要点
1.屏蔽一些有关控制终端操作的信号。防止在守护进程没有正常运转起来时,控制终端受到干扰退出或挂起。示例如下:
#include
#include
#include
void main(int argc, char ** argv){
time_t now;
int childpid, fd, fdtablesize;
int error, in, out;
/*忽略终端 I/O信号,STOP信号*/
signal(SIGTTOU,SIG_IGN);
signal(SIGTTIN,SIG_IGN);
signal(SIGTSTP,SIG_IGN);
signal(SIGHUP,SIG_IGN);
/*父进程退出,程序进入后台运行*/
if( fork()!=0 ) exit(1);
if( setsid()<0 ) exit(1); /*创建一个新的会议组*/
/*子进程退出,孙进程没有控制终端了*/
if( fork()!=0 ) exit(1);
if( chdir("/tmp")==-1 )exit(1);
/*关闭打开的文件描述符,包括标准输入、标准输出和标准错误输出*/
for( fd=0, fdtablesize=getdtablesize(); fd< fdtablesize;fd++) close(fd);
umask(0);/*重设文件创建掩模*/
signal(SIGCHLD,SIG_IGN);/*忽略SIGCHLD信号*/
/*打开log系统*/
syslog(LOG_USER|LOG_INFO,"守护进程测试!/n");
while(1){
time(&now);
syslog(LOG_USER|LOG_INFO,"当前时间:/t%s/t/t/n",ctime(&now));
sleep(6);
}
}
守护进程在Linux/Unix系统中有着广泛的应用。有时,开发人员也想把自己的程序变成守护进程。在创建一个守护进程的时候,要接触到子进程、进程组、会晤期、信号机制、文件、目录和控制终端等多个概念。因此守护进程还是比较复杂的,在这里详细地讨论Linux/Unix的守护进程的编写,总结出八条经验,并给出应用范例。
编程要点
1.屏蔽一些有关控制终端操作的信号。防止在守护进程没有正常运转起来时,控制终端受到干扰退出或挂起。示例如下:
signal(SIGTTOU,SIG_IGN); signal(SIGTTIN,SIG_IGN); signal(SIGTSTP,SIG_IGN); signal(SIGHUP ,SIG_IGN);
所有的信号都有自己的名字。这些名字都以“SIG”开头,只是后面有所不同。开发人员可以通过这些名字了解到系统中发生了什么事。当信号出现时,开发人员可以要求系统进行以下三种操作:
◆ 忽略信号。大多数信号都是采取这种方式进行处理的,这里就采用了这种用法。但值得注意的是对SIGKILL和SIGSTOP信号不能做忽略处理。
◆ 捕捉信号。最常见的情况就是,如果捕捉到SIGCHID信号,则表示子进程已经终止。然后可在此信号的捕捉函数中调用waitpid()函数取得该子进程的进程ID和它的终止状态。另外,如果进程创建了临时文件,那么就要为进程终止信号SIGTERM编写一个信号捕捉函数来清除这些临时文件。
◆ 执行系统的默认动作。对绝大多数信号而言,系统的默认动作都是终止该进程。
对这些有关终端的信号,一般采用忽略处理,从而保障了终端免受干扰。
这类信号分别是,SIGTTOU(表示后台进程写控制终端)、SIGTTIN(表示后台进程读控制终端)、SIGTSTP(表示终端挂起)和SIGHUP(进程组长退出时向所有会议成员发出的)。
2.将程序进入后台执行。由于守护进程最终脱离控制终端,到后台去运行。方法是在进程中调用fork使父进程终止,让Daemon在子进程中后台执行。这就是常说的“脱壳”。子进程继续函数fork()的定义如下:
#include <sys/types.h> #include <unistd.h> pid_t fork(void);
该函数是Linux/Unix编程中非常重要的函数。它被调用一次,但返回两次。这两次返回的区别是子进程的返回值为“0”,而父进程的返回值为子进程的ID。如果出错则返回“-1”。
3.脱离控制终端、登录会话和进程组。开发人员如果要摆脱它们,不受它们的影响,一般使用 setsid() 设置新会话的领头进程,并与原来的登录会话和进程组脱离。这只是其中的一种方法,也有如下处理的办法:
if ((fd = open("/dev/tty",O_RDWR)) >= 0) { ioctl(fd,TIOCNOTTY,NULL); close(fd); }
其中/dev/tty是一个流设备,也是终端映射,调用close()函数将终端关闭。
4.禁止进程重新打开控制终端。进程已经成为无终端的会话组长,但它可以重新申请打开一个控制终端。开发人员可以通过不再让进程成为会话组长的方式来禁止进程重新打开控制终端,需要再次调用fork函数。
上面的程序代码表示结束第一子进程,第二子进程继续(第二子进程不再是会话组长)。
5. 关闭打开的文件描述符,并重定向标准输入、标准输出和标准错误输出的文件描述符。进程从创建它的父进程那里继承了打开的文件描述符。如果不关闭,将会浪费系统资源,引起无法预料的错误。关闭三者的代码如下:
for (fd = 0, fdtablesize = getdtablesize(); fd < fdtablesize; fd++) close(fd);
但标准输入、标准输出和标准错误输出的重定向是可选的。也许有的程序想保留标准输入(0)、标准输出(1)和标准错误输出(2),那么循环应绕过这三者。代码如下:
for (fd =3, fdtablesize = getdtablesize(); fd < fdtablesize; fd++) close(fd);
有的程序有些特殊的需求,还需要将这三者重新定向。示例如下:
error=open("/tmp/error",O_WRONLY|O_CREAT, 0600); dup2(error,2); close(error); in=open("/tmp/in",O_RDONLY|O_CREAT,0600); if(dup2(in,0)==-1) perror("in"); close(in); out=open("/tmp/out",O_WRONLY|O_CREAT,0600); if(dup2(out,1)==-1) perror("out"); close(out);
6.改变工作目录到根目录或特定目录进程活动时,其工作目录所在的文件系统不能卸下。
一般需要将工作目录改变到根目录或特定目录,注意用户对此目录需要有读写权。防止超级用户卸载设备时系统报告设备忙。
7.处理SIGCHLD信号。SIGCHLD信号是子进程结束时,向内核发送的信号。
如果父进程不等待子进程结束,子进程将成为僵尸进程(zombie)从而占用系统资源。因此需要对SIGCHLD信号做出处理,回收僵尸进程的资源,避免造成不必要的资源浪费。可以用如下语句:
signal(SIGCHLD,(void *)reap_status);
捕捉信号SIGCHLD,用下面的函数进行处理:
void reap_status() { int pid; union wait status; while ((pid = wait3(&status,WNOHANG,NULL)) > 0) …… }
8.在Linux/Unix下有个syslogd的守护进程,向用户提供了syslog()系统调用。任何程序都可以通过syslog记录事件。
由于syslog非常好用和易配置,所以很多程序都使用syslog来发送它们的记录信息。一般守护进程也使用syslog向系统输出信息。syslog有三个函数,一般只需要用syslog(...)函数,openlog()/closelog()可有可无。syslog()在shslog.h定义如下: #include <syslog.h> void syslog(int priority,char *format,...);
其中参数priority指明了进程要写入信息的等级和用途。第二个参数是一个格式串,指定了记录输出的格式。在这个串的最后需要指定一个%m,对应errno错误码。
应用范例
如何实现在Linux下创建服务程序 |
---|
|
Linux系统能提供强大可靠的网络服务,并有管理程序对服务进行管理。例如我们熟悉的Web、FTP和电子邮件等,它们既可以单独运行,也可以被守护进程inetd调用,而且运行得都非常好。但我们不能仅停留在赞叹中,下面就给出两个服务程序程序和一个客户程序的例子,介绍服务程序和客户程序之间是 沟通的。另外还要编辑配置一些文件,让服务程序也能接受服务管理程序管理。 这两个服务程序功能相同,但一个是独立服务程序,另一个是被inetd调用的服务程序。这是TCP/IP网络服务的两大类,这里将两个程序放在一起是为了比较程序结构和运行方式。两服务程序都在Red Hat Linux 7.1和TurboLinux 7.0上调试通过。 独立服务器 TCP和UDP是两大TCP/IP数据传输方式,套接口是建立服务器客户机连接的机制,首先介绍它们建立通信联系的过程,然后给出一个TCP服务程序例子。 1.TCP套接口通信方式 对于TCP服务器端,服务程序首先调用建立套接口的函数socket(),然后调用绑定服务IP地址和协议端口号函数bind()。绑定成功后调用被动监听函数listen()等待客户连接,还要调用获取连接请求函数accept(),并一直阻塞到客户连接请求的到达,这个函数获取客户机IP地址和协议端口号。 对于TCP客户端,客户程序启动后后调用建立套接口函数socket(),然后调用连接函数connect(),此函数与服务器通过三次握手建立连接。 服务器和客户机建立连接后,就可以使用读函数read()和写函数write()收发数据了。数据交换完成后便各自调用关闭套接口函数close()删除套接口。 2.UDP套接口通信方式 UDP程序与TCP的区别是无需建立连接。服务器首先启动,然后等待用户请求。客户机启动后便直接向服务器请求服务,服务器接到请求后给出应答。 对于UDP服务器端,服务程序首先调用套接口函数socket(),然后调用绑定IP地址和协议端口号函数bind()。之后调用函数recvfrom()接收客户数据,调用sendto()向客户发送数据。 对于UDP客户端,客户机程序启动后调用套接口函数socket(),然后调用sendto()向服务器发送数据,调用recvfrom()接收服务器数据。 双方数据交换成功后,各自调用关闭套接口函数close()关闭套接口。UDP套接口通信方式见图2所示。 下面给出独立服务程序的例子。这个程序虽然简单,但是与复杂程序有着相同的结构。 //程序名:server.c //功能:服务器从客户机读入一个字符,并将排在此字符后面的字符回送客户机 //服务器端口:9000 #include "sys/types.h" #include "sys/socket.h" #include "stdio.h" #include "netinet/in.h" #include "arpa/inet.h" #include "unistd.h" int main() { int pid; //用于存放fork()执行结果 int server_sockfd,client_sockfd; //用于服务器和客户机套接口描述符 int bind_flag,listen_flag; //用于存放bind()和listen()执行结果 int server_address_length,client_address_length; //作为服务器客户机地址长变量 struct sockaddr_in server_address; //作为服务器地址结构变量(含地址和端口) struct sockaddr_in client_address; //作为客户机地址结构变量(含地址和端口) if((pid=fork())!=0) //用fork()产生新进程 exit(0) ; setsid() ; //以子进程开始下面的程序 函数socket(),创建一个套接口,成功则返回套接口描述符。 server_sockfd=socket(AF_INET,SOCK_STREAM,0); if(server_sockfd<0) { printf(“socket error /n”); exit(1); } server_address.sin_family=AF_INET; 函数htonl()用于将32位主机字节顺序转换为网络字节顺序,其中参数INADDR_ANY表示任何IP地址。 server_address.sin_addr.s_addr=htonl(INADDR_ANY); 函数htons()用于将16位主机字节顺序转换为网络字节顺序,其中的参数是绑定的端口号,读者可根据环境自行改动,目的是不与其它服务端口冲突。 server_address.sin_port=htons(9000); server_address_length=sizeof(server_address); 函数bind()用于绑定本地地址和服务端口号,若调用成功返回值为0。 bind_flag=bind(server_sockfd,/ (struct sockaddr *)&server_address,/ server_address_length); if(bind_flag<0) { printf(“bind error /n”); exit(1); } 函数listen(),指明服务器的队列长度,被动等待客户连接,调用成功返回值为0。 listen_flag=listen(server_sockfd,5); if(listen_flag<0) { printf(“listen error /n”); exit(1); } while(1) { char ch; 函数accept()等待和获取用户请求,为每个新连接请求创建一个新的套接口,调用成功返回新套接口描述符。 client_sockfd=accept(server_sockfd,/ (struct sockaddr *)&client_address,/ &client_address_length); 函数read()和write()用于在服务器和客户机之间传送数据,调用成功返回读和写的字节数。 函数close(),用于程序使用完一个套接口后关闭套接口,调用成功返回值0。其中的参数为accept()创建的套接口的描述符client_sockfd。 read(client_sockfd,&ch,1); printf(“cli_ch=%c”,ch); ch++; write(client_sockfd,&ch,1); close(client_sockfd); } } 程序完成后就可以使用命令进行编译。在命令行中输入“gcc -o server server.c”,将server.c编译成可执行程序server,这时便可用客户程序进行测试。在命令行执行“./server”启动服务程序,执行“netstat -na”查看有无server的服务端口。如果存在,则执行下面编写的客户程序“./client”。不过这仅是手工启动的方法,下面给出用服务管理程序管理server程序的方法。只要在目录/etc/rc.d/init.d下放入服务程序的脚本就能被服务程序读到。在命令行执行“touch server”创建文件server,并将文件属性改成可执行。在管理程序中并不能看到此服务名,脚本文件必须有一些结构才能被管理程序认为是服务程序脚本。 为了减少工作量,拷贝/etc/rc.d/init.d下脚本httpd,将拷贝脚本名命名为server,然后对其编辑。 (1)执行“cp httpd server”。 (2)用文本编辑器vi(其它编辑器亦可)将server打开进入编辑状态。首先用字符串server替换httpd。然后找到daemon server行,如果编写的程序放在变量PATH目录中,不需要修改此行;如果把服务程序放在其它目录中,就要写服务的全路径。例如程序在/root的目录中,就要写成daemon /root/server,还要删除“rm -f /var/run/server.pid”这一行。 (3)执行“chmod 755 server”,将server属性设定为可执行。 此时就可以用chkconfig、ntsysv等工具,在希望的运行级中增加这个新服务程序,然后测试客户机与服务器能否通信。 被xinetd调用的服务程序 在Linux系统中,有很多服务是被xinetd(较早版本使用的是inetd)超级守护服务器启动的。其实凡是基于TCP和UDP的服务都可使用超级守护进程启动,只是在服务量很大影响效率的情况下不被采用。 1.依赖xinetd启动的服务建立通信过程 为了与独立服务器程序比较,我们看一下依赖xinetd的服务器是如何启动的。 (1)xinetd启动时读取/etc/xinetd目录中的文件(早期版本为/etc/inetd文件),根据其中的内容给所有允许启动的服务创建一个指定类型的套接口,并将套接口放入select()中的描述符集合中。 (2)对每个套接口绑定bind(),所用的端口号和其它参数来自/etc/xinetd目录下每个服务的配置文件。 (3)如果是TCP套接口就调用函数listen(),等待用户连接。如果是UDP套接口,就不需调用此函数。 (4)所有套接口建立后,调用函数select()检查哪些套接口是活动的。 (5)若select()返回TCP套接口,就调用accept()接收这个连接。如果为UDP,就不需调用此函数。 (6)xinetd调用fork()创建子进程,由子进程处理连接请求。 ◆ 子进程关闭所有其它描述符,只剩下套接口描述符。这个套接口描述符对于TCP是accept()返回的套接口,对于UDP为最初建立的套接口。然后子进程连续三次dup()函数,将套接口描述符复制到0、1和2,它们分别对应标准输入、标准输出和标准错误输出,并关闭套接口描述符。 ◆ 子进程查看/etc/xinetd下文件中的用户,如果不是root用户,就用调用命令setuid和setgid将用户ID和组ID改成文件中指定的用户。 (7)对于TCP套接口,与用户交流结束后父进程需要关闭已连接套接口。父进程重新处于select()状态,等待下一个可读的套接口。 最后调用配置文件中指定的外部服务程序,外部程序启动后就可与用户进行信息传递了。 2.为xinetd编写专门的服务程序 除了独立服务程序能被xinetd启动外,还可以为xinetd编写专门的程序。此处的例子程序与上面server.c功能相同。不过两者的程序区别是很大的,此例的代码仅相当于上面传输数据的部分。我们还将程序名定为server.c,所以不能放在相同目录中,同名仅是为了和上面程序对照。 #include "unistd.h" int main() { char ch; read(0,&ch,1); ch++; write(1,&ch,1); } 将程序编译成可执行文件,并做些设置就可被xinetd启动。注意不要和上面的独立服务程序server一起启动,因为客户程序写得比较简单,访问的是固定端口,服务器都设成了相同的端口号。 (1)编辑/etc/services文件,在行末增加一条记录: server 9000/tcp (2)在目录/etc/xinetd.d下编写文件server,内容为: service server { disable = no socket_type = stream protocol = tcp wait = no user = root server = /home/test/server (此处设置成自己程序所在的目录) } 如果使用的是较早版本,则需在/etc/inetd.conf文件中添加下面的行: server tcp nowait root /path/to/yourdirectory/server (3)执行/etc/rc.d/initd.d/xinetd restart重新启动xinetd服务器。早期版本执行/etc/rc.d/initd.d/inetd restart重新启动inetd。 (4)执行netstat -an查看有没有server程序使用的端口号,如果有就可使用下面客户机程序进行测试了。 客户机程序 下面就客户机函数做一简单介绍。 //程序名client.c /*功能:从客户的控制台输入一个字符,然后将这个字符送到服务器,并将服务器返回的字符显示出来*/ #include "sys/types.h" #include "sys/socket.h" #include "stdio.h" #include "netinet/in.h" #include "arpa/inet.h" #include "unistd.h" int main() { int sockfd;// int address_len; int connect_flag; struct sockaddr_in address; int connect_result; char client_ch,server_ch; 函数socket()用于建立一个套接口,创建成功返回套接口描述符。 sockfd=socket(AF_INET,SOCK_STREAM,0); if(sockfd<0) { printf(“sockfd error /n”); } address.sin_family=AF_INET; address.sin_addr.s_addr=inet_addr(“192.168.0.1”);/*读者根据自己环境改成服务器地址*/ address.sin_port=htons(9000); address_len=sizeof(address); 函数connect()用于与服务器建立一个主动连接,调用成功返回值为0。 connect_flag=connect(sockfd,(struct sockaddr *)&address,address_len); if(connect_flag==-1) { perror(“client”); exit(1); } printf(“Input a character :”); 函数scanf()用于从控制台输入一个字符,并将字符存入client_ch的地址。函数write()和read()用于传输数据。函数printf()在客户机屏幕上显示服务器传回的字符。函数close()关闭套接口。 scanf(“%c”,&client_ch); write(sockfd,&client_ch,1); read(sockfd,&server_ch,1); printf(“character from server : %c/n”,server_ch); close(sockfd); exit(0); } 执行命令“gcc -o client client.c”,将client.c编译成client。执行“./client”,在程序提示下输入一个字符,就能看到服务器传回的字符。 以上介绍的仅是简单的例子。平时见到的服务程序远比它复杂,而且很多是多协议服务程序或是多协议多服务程序。多协议服务程序就是在main()中分别创建供服务的TCP和UDP套接口。为每个服务分别写出相应程序好处是便于控制,但是这样每个服务都启动两个服务器,而它们的算法响应是一样的,就要耗费不必要的资源,并且出了问题排错也较困难。多服务是将不同的服务集成在一起由一个程序完成,可用一个数组表示服务,数组中的每一项表示某协议某服务的一种,这样很容易扩展程序的服务功能。 |