1 僵尸进程产生的原因和避免方法
⑴僵尸进程产生的原因
当一个进程创建了一个子进程时,他们的运行是异步的。即父进程无法预知子进程会在什么时候结束,那么如果子进程退出,系统会回收其打开的文件和占用的内存等,但是仍然保留一些信息(如进程号pid等),这些保留的信息直到进程调用wait/waitpid时才会释放,如果父进程很繁忙来不及wait子进程,那么子进程保留的信息就不会被释放,比如进程号就回一直被占用,那么这个子进程就是一个僵尸进程。另外系统能使用的进程号是有限的,如果产生大量的僵尸进程,将导致系统没有可用的进程号而导致系统不能创建进程。
⑵僵尸进程的避免方法
①如果父进程不是很繁忙,我们可以通过直接调用wait/waitpid来等待子进程结束,当然这会导致父进程被挂起。比如,父进程循环3次,子进程循环5次,父进程先结束,但是它不忙,父进程调用wait挂起等待子进程结束,子进程结束后回首所有的东西。就不会出现僵尸进程。
②如果父进程很忙,不能让父进程一直挂起直到子进程结束,那么我们可以用信号函数sugaction向父进程发送一个设置为SIG_IGN的SIGCHILD信号,这样,子进程结束后,父进程就会收到子进程结束的信号,并调用wait回收子进程的资源。
2 孤儿进程产生的原因
如果父进程先结束,但是父进程没有调用wait/waitpid来等待子进程的结束,子进程还在运行,此时的子进程是孤儿进程。因为每个进程结束时,系统都会扫描当前系统中运行的所有进程,看看有没有哪个进程是刚刚结束的这个进程的子进程,如果有,就由init来接管它,成为他的父进程。
3 守护进程
⑴守护进程特性
①守护进程最重要的特性就是后台运行。
a任何一个进程都可以后台运行,执行的时候后面加上&即可,但是后台进程会随着shell的退出而结束;
b 如果一个进程想永远在后台运行,不受shell退出的影响,正统的做法就是把它创建为守护进程(脱离自己的父进程);
②守护进程要关闭从父进程中继承下来的一些东西,包括(未关闭的文件描述符,控制终端,会话和进程组,工作目录及文件创建掩码等)。
③守护进程的启动方式很多,可以从Linux系统启动时从启动脚本/etc/rc.d中启动,可以由作业规划进程crond启动,还可以从用户终端(shell)执行。
⑵进程查看
守护进程:ps –x (x显示没有控制终端的进程)
普通进程:ps -a或者带参数的其他ps命令 (a显示终端上正在运行的进程)
后台进程:jobs 或者ps –x等 (r只显示正在运行的进程)
⑶守护进程编程要点
①把进程放在后台运行
做法:进程中调用fork(),并且使父进程终止,从而让daemon在子进程中后台执行。
if(pid=fork())
exit(0);//结束父进程,子进程继续
②让守护进程脱离从父进程原来的登陆会话,进程组和控制终端
之前父进程和子进程运行在同一个会话里,如果父进程结束,那么子进程被init程序托管,他不是组长没有可运行的会话,调用setsid(); 子进程成为新的会话组长,与原来的会话和进程组脱离,由于会话独占一个控制终端,所以业余控制终端脱离。
③禁止重新打开控制终端
由于子进程现在已经是进程组长,所以它可以重新打开一个控制终端,为了不让他有此功能,if(pid=fork()) exit(0); //结束第一子进程,第二子进程继续(第二子进程不再是会话组长)
④关闭从父进程继承的文件描述符
进程从创建它的父进程那里继承了打开的文件描述符。如不关闭,将会浪费系统资源,造成进程所在的文件系统无法卸下以及引起无法预料的错误。按如下方法关闭它们:for(i=0;i< NOFILE;++i)//关闭打开的文件描述符
close(i);
⑤ 改变当前工作目录
进程活动时,其工作目录所在的文件系统不能卸下。一般需要将工作目录改变到根目录。对于需要转储核心,写运行日志的进程将工作目录改变到特定目录如/tmpchdir("/")
⑥重设文件创建掩模
进程从创建它的父进程那里继承了文件创建掩模。它可能修改守护进程所创建的文件的存取位。为防止这一点,将文件创建掩模清除:umask(0);
⑦处理SIGCHLD信号
在Linux下可以简单地将SIGCHLD信号的操作设为SIG_IGN。
signal(SIGCHLD,SIG_IGN);
这样,内核在子进程结束时不会产生僵尸进程。
4 IO模型
Select,poll和epoll三种IO多路复用模型就是通过一种机制,监视多个描述符,一旦某个描述符就绪(读写或者异常)就通知进程进行对应的读写操作。
一select
使用情况:该函数准许进程指示内核等待多个事件中的任何一个发送,并只在一个或者多个事件发生或经历一段指定的时间后才唤醒。
工作原理:select先把文件描述符从用户区拷贝到内核,判断有没有就绪的fd,如果有就唤醒在队列中等待的进程,如果超过一段时间还没有被唤醒,则调用select的进程会重新被唤醒获得CPU,重新遍历fd最后把描述符从内核拷贝到用户空间。
函数原型如下:
Intselect(int maxfdp, fd_set *readfds, fd_set *writefds, fd_set *errorfds,structtimeval*timeout)
①fd_set结构体存放有事件的文件描述符个数,及socket数组
1typedef struct fd_set
{
u_int fd_count; // how many are SET?
SOCKET fd_array[FD_SETSIZE]; // an array of SOCKETs
}fd_set;
2 fd_set集合可以通过一些宏由人为来操作:
FD_ZERO(fd_set*);清空集合
FD_SET(int,fd_set*);将一个给定的文件描述符加入集合之中
FD_CLR(int,fd_set*);将一个给定的文件描述符从集合中删除
FD_ISSET(int ,fd_set* );检查集合中指定的文件描述符是否可以读写
②readfds,writefds,errorfds三个参数
fd_set*readfds,writefds,errorfds是指向fd_set结构的指针,这个集合中应该包括文件描述符,我们是要监视这些文件描述符的读变化的,即我们关心是否可以从这些文件中读取数据了,如果这个集合中有一个文件可读,select就会返回一个大于0的值,表示有文件可读,如果没有可读的文件,则根据timeout参数再判断是否超时,若超出timeout的时间,select返回0,若发生错误返回负值。可以传入NULL值,表示不关心任何文件的读变化。
③structtimeval *timeout是select的超时时间,这个参数至关重要,它可以使select处于三种状态:
(1)永远等待下去:仅在有一个描述字准备好I/O时才返回。为此,把该参数设置为空指针NULL。
(2)等待一段固定时间:在有一个描述字准备好I/O时返回,但是不超过由该参数所指向的timeval结构中指定的秒数和微秒数。
(3)根本不等待:检查描述字后立即返回,这称为轮询。为此,该参数必须指向一个timeval结构,而且其中的定时器值必须为0。
④Select系统调用(返回值)
Select调用返回时,返回值有如下情况:
1)正常情况下返回满足要求的文件描述符个数;
2)经过了timeout等待后仍无文件满足要求,返回值为0;
3)如果select被某个信号中断,它将返回-1并设置errno为EINTR。
4)如果出错,返回-1并设置相应的errno。
⑤Select系统调用(使用方法)
1)将要监控的文件添加到文件描述符集
2)调用Select开始监控
3)判断文件是否发生变化
系统提供了4个宏对描述符集进行操作:
#include <sys/select.h>
void FD_SET(int fd, fd_set *fdset)
void FD_CLR(int fd, fd_set*fdset)
void FD_ZERO(fd_set *fdset)
void FD_ISSET(int fd, fd_set *fdset)
宏FD_SET将文件描述符fd添加到文件描述符集fdset中;
宏FD_CLR从文件描述符集fdset中清除文件描述符fd;
宏FD_ZERO清空文件描述符集fdset;
在调用select后使用FD_ISSET来检测文件描述符集fdset中的文件fd发 生了变化。
FD_ZERO(&fds); //清空集合
FD_SET(fd1,&fds); //设置描述符
FD_SET(fd2,&fds); //设置描述符
maxfdp=fd1+1; //描述符最大值加1,假设fd1>fd2
switch(select(maxfdp,&fds,NULL,NULL,&timeout))
case -1:exit(-1);break; //select错误,退出程序
case 0:break;
default:
if(FD_ISSET(fd1,&fds)) //测试fd1是否可读
二poll
工作原理:poll先把文件描述符从用户区拷贝到内核,判断有没有就绪的fd,如果有就唤醒在队列中等待的进程,如果超过一段时间还没有被唤醒,则调用poll的进程会重新被唤醒获得CPU,重新遍历fd,判断有没有就绪的fd,最后把描述符从内核拷贝到用户空间。
# include<poll.h>
int poll ( struct pollfd * fds, unsigned int nfds, int timeout);
①pollfd结构体定义如下:
struct pollfd {
int fd; /* 文件描述符 */
short events; /* 等待的事件 */
short revents; /* 实际发生了的事件 */
} ;
每一个pollfd结构体指定了一个被监视的文件描述符,可以传递多个结构体,指示poll()监视多个文件描述符。每个结构体的events域是监视该文件描述符的事件掩码,由用户来设置这个域。revents域是文件描述符的操作结果事件掩码,内核在调用返回时设置这个域。events域中请求的任何事件都可能在revents域中返回。合法的事件如下:
POLLIN 有数据可读。
POLLRDNORM 有普通数据可读。
POLLRDBAND 有优先数据可读。
POLLPRI 有紧迫数据可读。
POLLOUT 写数据不会导致阻塞。
POLLWRNORM 写普通数据不会导致阻塞。
POLLWRBAND 写优先数据不会导致阻塞。
POLLMSGSIGPOLL 消息可用。
此外,revents域中还可能返回下列事件:
POLLER 指定的文件描述符发生错误。
POLLHUP 指定的文件描述符挂起事件。
POLLNVAL 指定的文件描述符非法。
这些事件在events域中无意义,因为它们在合适的时候总是会从revents中返回。
②timeout参数指定等待的毫秒数,无论I/O是否准备好,poll都会返回。timeout指定为负数值表示无限超时,使poll()一直挂起直到一个指定事件发生;timeout为0指示poll调用立即返回并列出准备好I/O的文件描述符,但并不等待其它的事件。这种情况下,poll()就像它的名字那样,一旦选举出来,立即返回。
三epoll接口
⑴epoll操作过程需要三个接口,分别如下:
#include <sys/epoll.h>
①intepoll_create(int size);
建立一个epoll对象,参数size是内核保证能够正确处理的最大句柄数,多余这个最大句柄数时内核不保证有效;
②int epoll_ctl(intepfd, int op, int fd, struct epoll_event *event);
(可以操作上面创建的epoll,例如,将刚创建的socket加入到epoll中让其监控,或者把epoll正在监控的某个socket句柄移出epoll,不再监控他等等。)
第一个参数是epoll_create()的返回值,
第二个参数表示动作,用三个宏来表示:
EPOLL_CTL_ADD:注册新的fd到epfd中;
EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
EPOLL_CTL_DEL:从epfd中删除一个fd;
第三个参数是需要监听的fd,
第四个参数是告诉内核需要监听什么事,struct epoll_event结构如下:
struct epoll_event {
__uint32_tevents; /* Epoll events */
epoll_data_t data; /* User data variable */
};
events可以是以下几个宏的集合:
EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
③int epoll_wait(intepfd, struct epoll_event * events, int maxevents, int timeout);
(调用时,在给定的timeout时间内,当在监控的所有句柄中有事件发生时,就返回用户态的进程。)
等待事件的产生,类似于select()调用。参数events用来从内核得到事件的集合,maxevents告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size,参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。该函数返回需要处理的事件数目,如返回0表示已超时。
⑵工作模式
epoll对文件描述符的操作有两种模式:LT(level trigger)和ET(edge trigger)。LT模式是默认模式,LT模式与ET模式的区别如下:
LT模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll_wait时,会再次响应应用程序并通知此事件。
ET模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件。
ET模式在很大程度上减少了epoll事件被重复触发的次数,因此效率要比LT模式高。epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。
⑶总结:
①select的几大缺点
i 每次调用select都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多的时候会很大
ii 每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多的
iii select默认支持的文件描述符数量是1024
②poll的实现和select非常相似,只是描述fd集合的方式不同,poll使用pollfd结构,select是使用fd_set结构。其他的都差不多。
③epoll对select和poll三个缺点的处理
select和poll都只提供了一个函数——select或者poll函数。而epoll提供了三个函数,epoll_create,epoll_ctl和epoll_wait,epoll_create是创建一个epoll句柄;epoll_ctl是注册要监听的事件类型;epoll_wait则是等待事件的产生。
对于第一个缺点,epoll的解决方案在epoll_ctl函数中。每次注册新的事件到epoll句柄中时(在epoll_ctl中指定EPOLL_CTL_ADD),会把所有的fd拷贝进内核,而不是在epoll_wait的时候重复拷贝。epoll保证了每个fd在整个过程中只会拷贝一次。
对于第二个缺点,epoll的解决方案不像select或poll一样每次都把current轮流 fd对应的设备等待队列中,而只在epoll_ctl时把current挂一遍(这一遍必不可少)并为每个fd指定一个回调函数,当设备就绪,唤醒等待队列上的等待者时,就会调用这个回调函数,而这个回调函数会把就绪的fd加入一个就绪链表)。epoll_wait的工作实际上就是在这个就绪链表中查看有没有就绪的fd(利用schedule_timeout()实现睡一会,判断一会的效果)。
对于第三个缺点,epoll没有这个限制,它所支持的FD上限是最大可以打开文件的数目,这个数字一般远大于2048,举个例子,在1GB内存的机器上大约是10万左右,具体数目可以cat /proc/sys/fs/file-max察看,一般来说这个数目和系统内存关系很大。
④总结:
(1)select,poll实现需要自己不断轮询所有fd集合,直到设备就绪,期间可能要睡眠和唤醒多次交替。而epoll其实也需要调用epoll_wait不断轮询就绪链表,期间也可能多次睡眠和唤醒交替,但是它是设备就绪时,调用回调函数,把就绪fd放入就绪链表中,并唤醒在epoll_wait中进入睡眠的进程。虽然都要睡眠和交替,但是select和poll在“醒着”的时候要遍历整个fd集合,而epoll在“醒着”的时候只要判断一下就绪链表是否为空就行了,这节省了大量的CPU时间。这就是回调机制带来的性能提升。
(2)select,poll每次调用都要把fd集合从用户态往内核态拷贝一次,并且要把current往设备等待队列中挂一次,而epoll只要一次拷贝,而且把current往等待队列上挂也只挂一次(在epoll_wait的开始,注意这里的等待队列并不是设备等待队列,只是一个epoll内部定义的等待队列)。这也能节省不少的开销。
5 线程资源回收的方法:
⑴资源:①进程中所有线程共享的资源
②线程私有的资源(栈)
⑵线程退出的方式:①return
②pthread_exit
③pthread_cancel等
⑶线程分为两种:一种是joinable(可结合的)和detached(分离的)。如果在创建线程时没有把线程属性改为分离态,它默认是可结合的,那么可结合的线程在线程退出后不会立即释放资源,必须使用pthread_join来回收资源;而分离的线程在线程退出时系统会自动回收资源,如果是分离的,不能调用pthread_join。
⑷可结合线程退出和资源回收:
①子线程使用return退出,主线程中使用pthread_join回收资源;
②子线程使用pthread_exit退出,主线程中使用pthread_join接收pthread_exit返回值,并回收资源;
③主线程中使用pthread_cancel退出,然后调用pthread_join回收资源。
6