开发者社区> 技术小阿哥> 正文

Unix环境高级编程:进程控制-线程控制-僵尸进程

简介:
+关注继续查看

一、进程间通讯:

1、信号
SIGHUP:挂断终止信号。内核信号。当终止一个终端时,内核就把这一种信号发送给该终端所控制的所有进程。通常情况下,一个进程组的控制终端是该用户拥有的终端,但不完全是如此;当进程组的首进程结束时,就会向该进程组的所有进程发送这种信号。这就可以保证当一个用户退出使用时,其后台进程被终止,除非有其它方面的安排。
SIGINT:中断终止信号。内核信号。当一个用户按了中断键(一般为Ctrl+C)后,内核就向与该终端有关联的所有进程发送这种信号。它提供了中止运行程序的简便方法。
SIGQUIT:退出终止信号。内核信号。这种信号与SIGINT 非常相似,当用户按了退出键时(为ASCII 码FS,通常为Ctrl+\),内核就发送出这种信号.
SIGILL:非法指令终止信号。内核信号。当一个进程企图执行一条非法指令时,内核就发出这种信号。例如,在没有相应硬件支撑的条件下,企图执行一条浮点指令时,则会引起这种信号的发生。SIGILL 和SIGQUIT一样,也形成非正常终止。
SIGTRAP:陷阱终止信号。内核信号。这是一种由调试程序使用的专用信号。由于他的专用行和特殊性,我们不再对它作进一步的讨论。SIGTRAP 也形成非正常终止。
SIGFPE:浮点处理出错终止信号。内核信号。当产生浮点错误时(比如溢出),内核就发出这种信号,它导致非正常终止。
SIGKILL:杀死终止信号。用户信号。这是一个相当特殊的信号,它从一个进程发送到另一个进程,使接收到该信号的进程终止。内核偶尔也会发出这种信号。SIGKILL 的特点是,它不能被忽略和捕捉,只能通过用户定义的相应中断处理程序而处理该信号。因为其它的所有信号都能被忽略和捕捉,所以只有这种信号能绝对保证终止一个进程。
SIGALRM:闹钟信号。内核信号。当一个定时器到时的时候,内核就向进程发送这个信号。定时器是由改进程自己用系统调用alarm()设定的。
SIGTERM:终止指定进程信号。用户信号。这种信号是由系统提供给普通程序使用的,按照规定,它被用来终止一个进程。
SIGSTOP:暂停信号。这个信号使进程暂时中止运行,系统将控制权转回正在等待运行的下一个进程。
SIGCHLD:子进程结束-通知信号。内核信号。UNIX 中用它来实现系统调用exit()和wait()。执行exit()时,就向子进程的父进程发送SIGCHLD 信号,如果这时父进程政在执行wait(),则它被唤醒;如果这时候父进程不是执行wait(),则此父进程不会捕捉SIGCHLD 信号,因此该信号不起作用,
SIGUSR1,SIGUSR2:用户信号,非内核发送。

信号的处理方式有3种:函数处理,SIG_IGN忽略,SIG_DFL恢复系统对信号的默认处理方式。
signal(信号,处理方式)指明对信号的处理方式。
kill(),raise()用于进程间发送信号。raise允许发给自身进程。
2、管道
pipe(fd[])函数建立管道。
dup(),dup2()可重定向管道
p=popen('命令',mode)将命令的执行结果输入/输出到管道p。这样在进程中可以读写该管道。pclose关闭管道。
pipe_fp = popen("sort", "w")对一次写入pipe_fp管道的内容进行排序。
3、有名管道
mknod(“name”,权限,0)和mkfifo()创建管道。
fs=fopen(fifo_name,"r")
fgets(readbuf, 80, fs);
fputs(readbuf, 80, fs);
fclose(fs);
限制:有名管道同时有读写2个端口,如果进程试图向一个没有读端口的管道写入数据,那么该进程将会收到一个来自内核的SIGPIPE信号。
4、文件锁和记录锁
SYSTEM V记录锁定:
文件锁:锁定整个文件 
记录锁:锁定部分文件内容
lockf(fd,opt,size):opt:F_ULOCK为先前锁定的区域解锁;F_LOCK:锁定一个区域;F_TLOCK测试并锁定一个区域;F_TEST测试一个区域是否上锁。
size从当前文件指针开始锁定的文件区域,=0表示锁定从当前到文件尾。
锁定:lseek(fd,0L,0);(lockf(fd,F_LOCK,0L)
解锁:lseek(fd,0L,0);(lockf(fd,F_ULOCK,0L)
BSD咨询式文件锁定:
flock(fd,opt)。LOCK_SH阻塞性共享锁;LOCK_EX阻塞性互斥锁;LOCK_SH|LOCK_NB非阻塞性共享锁;LOCK_EX|LOCK_NB非阻塞性互斥锁;LOCK_UN解锁
5、临界区

    可以禁止任何CPU调度,实现多进程和多线程之间的互斥。

启动临界区只需要关中断即可。

local_irq_disable()//屏蔽中断
... //临界区
local_irq_enable()//开中断
6、System V IPC
System V IPC机制都需要生成关键字,key_t ftok ( char *pathname, char proj );可生成关键字,mykey = ftok("/tmp/myapp", 'a');
1)消息队列:
int msgget(key_t key,int msgflg)创建或获取消息队列
int msgsnd(int msqid,struct msgbuf *msgp,int msgsz,int msgflg)向消息队列发送消息
int msgrcv(int msqid,struct msgbuf *msgp,int msgsz,long mtype,int msgflg )从消息队列读取消息
int msgctl(int msgqid,int cmd,struct msqid_ds *buf )控制消息队列对象的行为
2)信号量
int semget(key_t key,int nsems,intsemflg)创建或获取信号量
int semop(int semid,struct sembuf *sops,unsigned nsops)读或写信号量
int semctl(int semid,int semnum, int cmd, union semun arg )控制信号量对象的行为
3)共享内存
int shmget(key_t key,int size,int shmflg);创建或获取共享内存
int shmat(int shmid,char *shmaddr,int shmflg);连接共享内存到进程空间
int shmdt( char *shmaddr);断开共享内存与进程空间的连接
int shmctl(int shmqid, int cmd, struct shmid_ds *buf );控制共享内存行为
需要文件锁或信号量来保证共享内存的访问互斥


二、线程控制:

1.线程基本操作
pthread_t pthread_self(void)线程获取自身的线程ID
int pthread_create(pthread_t *tid,pthread_attr_t *attr,void*(*func)(),void *arg)创建线程;pthread_create(&tid,0,func,0)
void pthread_exit(void*rval_ptr)线程退出,返回退出值
int pthread_join(pthread_t tid,void **rval_ptr)等待线程终止,并接收退出值
void pthread_cleanup_push(void(*rtn)(void*),void*arg)可以在线程中使用该函数挂载一个或多个在线程结束时执行的函数。使用pthread_cleanup_pop(0)在pthread_exit前多次调用,可以倒序执行挂载的函数。
int pthread_detach(pthread_t tid)使一个线程变成脱离线程。分离后不能使用pthread_join等待其终止
2.互斥量mutex
pthread_mutex_t mutex;
int pthread_mutex_init(pthread_mutex_t *mutex,pthread_mutexattr_t*attr)初始化互斥量
int pthread_mutex_destroy(pthread_mutex_t *mutex)删除该互斥量,释放空间
int pthread_mutex_lock(pthread_mutex_t *mutex)加锁(阻塞)
int pthread_mutex_trylock(pthread_mutex_t *mutex)加锁(非阻塞)
int pthread_mutex_unlock(pthread_mutex_t *mutex)解锁
在线程中连续加锁两次将造成死锁。(但是如果该锁具有递归的属性,则不会,取决于实现方式)
死锁的条件:独占资源,循环等待,不可剥夺
3.读写锁rwlock:(拥有更高的并行性:当以读模式锁定时,是共享锁;当以写模式锁定时,是独占锁)
pthread_rwlock_t rwlock;
int pthread_rwlock_init(pthread_rwlock_t *rwlock,pthread_rwlockattr_t*attr)初始化互斥量
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock)删除该互斥量,释放空间
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock)加锁(只阻塞写锁)
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock)加锁(阻塞读和写锁)
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock)加锁(非阻塞)
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock)加锁(非阻塞)
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock)解锁
4.条件变量:提供了多线程会合的场所(只能与mutex共同使用)
pthread_cond_t cond;
int pthread_cond_init(pthread_cond_t *cond,pthread_condattr_t*attr)初始化条件量
int pthread_cond_destroy(pthread_cond_t *cond)删除该条件变量,释放空间
int pthread_cond_wait(pthread_cond__t *cond,pthread_mutex_t *mutex)等待条件量为true   
pthread_cond_wait原理:传递给pthread_cond_wait()的互斥量mutex对条件进行保护,调用者把锁住的互斥量(必须是已经锁住的)传给
函数pthread_cond_wait()后,该函数将把本线程放到等待条件的线程列表上,然后对信号量解锁(内部完成),这都是原子操作。(解锁的目的是
为了让其他线程上锁->改变条件->并给该线程发唤醒信号),直到等到唤醒信号或超时,又将mutex上锁,并返回函数。
int pthread_cond_timewait(pthread_cond__t *cond,pthread_mutex_t *mutex,struct timespec *timeout)在规定时间内等待条件量为true
int pthread_cond_signal(pthread_cond_t *cond)传递线程条件为true,使某一个线程运行
int pthread_cond_broadcast(pthread_cond_t *cond)传递线程条件为true,使所有线程运行
int pthread_mutex_unlock(pthread_mutex_t *mutex)解锁
5.线程属性控制
pthread_attr_t attr;
int pthread_attr_init(pthread_attr_t *attr)初始化线程属性,使用系统默认配置
int pthread_attr_destroy(pthread_attr_t *attr)删除该线程属性,释放空间
对变量attr的读写操作有较多的函数


条件变量例子

通过条件变量主线程可以向其他线程发送信号,启动其他线程继续执行。
#include <pthread.h>
struct msg{
 struct msg *m_nest;
 /*...more stuff here...*/
}

struct msg *workq;//工作队列,作为线程要保护的条件(驱动线程的运行)
/*条件标识qready,当条件发送时,将置该标记*/
pthread_cond_t   qready=PTHREAD_COND_INITIALIZER;//相当于调用初始化函数
pthread_mutex_t  qlock =PTHREAD_MUTEX_INITIALIZER;

void process_msg(void)
{
 struct msg *mp;
while(true)
{
 pthread_mutex_lock(&qlock); //锁住workq,
 while(workq==NULL)  //判断条件发生
    pthread_cond_wait(&qready,&qlock);//没发生,等待->执行过程:解锁qlock->将本线程放入等待列表->阻塞 ...signal条件变化(不一定满足作者需求)->上锁qlock->返回
    //pthread_cond_wait执行过程
//条件已经满足,无需进行等待
 mp=workq;
 workq=mp->m_next;
 pthread_mutex_unlock(&qlock);
/...more process the message mp.../
}
}

void enqueue_msg(struct msg *mp)
{
 pthread_mutex_lock(&qlock); //改变条件workq时,要加锁
 mp->m_next = workq;
 workq=mp;
 pthread_mutex_unlock(&qlock);
 pthread_cond_signal(&qready);//条件已经改变,可以通知该条件的线程等待队列。
}

无论在哪个线程中,对条件workq的访问必须加锁。
条件变量互斥的例子必须有三个全局变量要素:
1.mutex  用于保护条件的修改和访问、以及参与_wait函数的执行
2.pthread_cond_t cond_flag; 条件标志,用以通知其他线程条件已经已经发生
3.条件:可以是作者选定的任意变量,它的任何访问必须用mutex保护



三、条件变量使用模型(适用于线程池、消息队列等)

    条件变量适用场景:条件变量的作用是用于多线程之间关于共享数据状态变化的通信。当一个或多个动作需要另外一个或多个动作完成时才能进行,即:当一个或多个线程的行为依赖于另外一个或多个线程对共享数据状态的改变时,这时候就可以使用条件变量。不管是生产者还是消费者,谁获取了mutex,谁就能对资源操作(读或写)。当资源可用条件满足时,等待队列将以此退出等待。 若signal到来(即条件满足)但是等待队列中没有wait,那么将不会有任何改变,signal不会被缓存。

                wKiom1XhKK7A80ehAAFKmmHrTpU225.jpg

                             条件变量的使用模型

1.资源生产者和消费者同时竞争mutex(守护对资源和资源标识的修改)

2.如果资源生产者获取到mutex,执行第1步,则生产者向资源队列中插入资源,并标识资源可用,然后向消费者发送条件已满足信号signal,并释放mutex。

3.如果资源消费者获取到mutex,执行第2步,则消费者判断资源是否可用,如果不可用,则wait直到资源可用(在wait的实现中释放了mutex,当接收到signal条件满足时并返回时,又锁定了mutex。wait有一个等待队列,所有的消费者都可能在wait队列中等待条件或mutex);如果资源直接就是可用的,那么无须调用wait,直接使用资源,然后改变资源可用标识(判断资源队列中是否还有可用资源),释放mutex。

 

四、多进程和多线程对管道、共享内存、消息队列的访问

pipe和FIFO特性:
*read: 对一个空的、阻塞的FIFO的read调用将阻塞,直到存在数据可以读(至少1个)时才继续执行,并返回读到的数据个数(数据不一定完全读取);而对于一个空的非阻塞的FIFO的read调用将立即返回0
*write:对阻塞的管道write调用将阻塞,直到数据可以写入时才继续执行,并返回写入数据个数(数据不一定完全写入)
    对非阻塞的管道write调用,如果管道不能接收所有写入的数据,将按一下规则执行:
     *如果请求写入的数据小于等于PIPE_BUF,调用失败,数据不能写入
     *如果请求写入的数据大于等于PIPE_BUF,试图写入全部数据,返回实际写入字节数
*管道读写的原子化要求(用于多进程和多线程同时读写管道时,保证互斥访问):阻塞,写请求的数据长度必须小于等于PIPE_BUF
system V IPC

信号量:使用临界区实现了对信号量put和get操作,用于多进程和多线程同时读写管道时,保证了互斥访问
共享内存:不提供多进程或多线程对共享内存访问的互斥机制,需要作者自己建立同步机制,可以使用信号量或文件锁或临界区

消息队列:不提供多进程或多线程对消息队列访问的互斥机制,需要作者自己建立同步机制,可以使用信号量或文件锁或临界区

五、僵尸进程

1.僵尸进程的定义:僵尸进程在Linux中是一个非常可怕的进程,僵尸进程是指已经完全退出但是没有被父进程调用wait或waitpid进行回收的进程。僵尸进程经持续占用系统资源。在每个进程退出的时候,内核释放该进程所有的资源,包括打开的文件,占用的内存等。 但是仍然为其保留一定的信息(包括进程ID,退出状态,运行时间等)。直到父进程通过wait/waitpid来取时才释放。

2.僵尸进程的查看:ps -lax状态为Z+或Z或Zs的进程就是僵尸进程


3.产生僵尸进程实例:

#include <unistd.h>  

#include <stdio.h>   

#include <stdlib.h>

int main ()   

{   

    pid_t fpid;  

    int count=0;  

    fpid=fork();   

 while(1){

    if (fpid < 0)   

        printf("error in fork!");   

    else if (fpid == 0) {  

        printf("i am the child process, my process id is %d/n",getpid());   

exit(0);

    }  

    else {  

        sleep(3); 

    }  

 }

    return 0;  

}  


因为父进程没有回收子进程的资源,导致产生大量的僵尸进程。

4.解决僵尸进程的方法

a.父进程调用wait或waitpid回收子进程

pid_t wait(int *status);等待某个子进程的退出,会导致当前进程阻塞;status为子进程返回状态

pid_t wwaitpid(pid_t pid,int *status,int options);  

pid=-1时,相当于wait

pid=0时,等待当前进程组中的某个进程的退出,可以替不是自己的子进程收尸

pid=-x时,等待|-x|即x进程组的某个进程退出,为其收尸

pid=x时,等待自己的子进程pid的退出,为其收尸

options=WNOHANG,使得waitpid调用不会阻塞,而是立即返回,不会一直等待子进程退出为其收尸;

options=0,不关心

b.SIGCHLD信号处理

子进程在退出时都会自动发送SIGCHLD信号,父进程可以处理该信号,调用wait回收子进程。

当已经有SIGCHLD信号时,我们调用waitpid是可以立即返回的。所以经常在SIGCHLD处理程序中调用waitpid函数,这样就可以期望他总能立即返回,但是如果在执行SIGCHLD处理程序期间又有子进程终止,因为unix不对信号排队,如果多于一个子进程终止,则会导致信号丢失,在这种情况下,如果只调用一次waitpid就会导致僵死进程的产生,可以采取while(waitpid(-1,0,WNOHANG)>0);来避免这个问题。还有一种方式是将options设置为WNOHANG在调用waitpid也会立即返回。

c.让父进程退出,从而子进程将脱离父进程,init进程将成为该进程的父进程,如果该进程退出,init将及时的回收该进程资源,巧妙的设计可以实现这一点:

int main ()   

{   

    pid_t fpid;  

    int count=0;  

     

 while(1){

    fpid=fork();  

    if (fpid < 0)   

        printf("error in fork!");   

    else if (fpid == 0) {#子进程  

        printf("i am the child process, my process id is %d/n",getpid());   

        exit(0);

    }  

    else {  #父进程

            fpid=fork();  

            if (fpid < 0)   

                printf("error in fork!");   

            else if (fpid > 0) { 

            exit(0);//让父进程退出保留子进程 

            }  

    }  

 }

    return 0;  




本文转自 a_liujin 51CTO博客,原文链接:http://blog.51cto.com/a1liujin/1683132,如需转载请自行联系原作者

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

相关文章
SAS学习笔记之《SAS编程与数据挖掘商业案例》(4)DATA步循环与控制、常用全程语句、输出控制
SAS学习笔记之《SAS编程与数据挖掘商业案例》(4)DATA步循环与控制、常用全程语句、输出控制 1. 各种循环与控制 DO组 创建一个执行语句块 DO循环 根据下标变量重复执行DO和END之间的语句 DO WHILE 重复执行直到条件为假则退出循环 DO UNTIL 重复执行直到条件为真则退出循环 DO OVER 对隐含下标
1314 0
【Android 逆向】Android 进程注入工具开发 ( 调试进程中寄存器的作用 | 通过 EIP 寄存器控制程序运行 | EIP 寄存器的存档与恢复 )
【Android 逆向】Android 进程注入工具开发 ( 调试进程中寄存器的作用 | 通过 EIP 寄存器控制程序运行 | EIP 寄存器的存档与恢复 )
26 0
《UNIX网络编程 卷2:进程间通信(第2版)》——1.7 Unix标准
Posix是“可移植操作系统接口”(Portable Operating System Interface)的首字母缩写。它并不是一个单一标准,而是一个由电气与电子工程师学会即IEEE开发的一系列标准。
1480 0
Linux下C编程,子进程创建函数fork() 执行解析
最近在看进程间的通信,看到了fork()函数,虽然以前用过,这次经过思考加深了理解。现总结如下: 1.函数本身   (1)头文件   #include  #include   (2)函数原型   pid_t fork( void);  (pid_t 是一个宏定义,其实质是int 被定义在#include中)  返回值: 若成功调用一次则返回两个值,子进程返回0,父进程返回子进程ID;否则,出错返回-1   (3)函数说明   一个现有进程可以调用fork函数创建一个新进程。
756 0
《UNIX网络编程 卷2:进程间通信(第2版)》——1.9 小结
各种类型IPC的持续性可以是随进程持续的、随内核持续的或随文件系统持续的,这取决于IPC对象存在时间的长短。在为给定的应用选择所用的IPC类型时,我们必须清楚相应IPC对象的持续性。
1329 0
Linux运行与控制后台进程的方法:nohup, setsid, &, disown, screen
我们经常会碰到这样的问题,用ssh登录了远程的Linux服务器,运行了一些耗时较长的任务,结果却由于网络等的不稳定导致任务中途失败。这是由于在用户注销(logout)或者网络断开时,终端会收到 HUP(hangup)信号从而关闭其所有子进程。
1006 0
Linux下C编程,进程通信之无名管道通信
最近在看进程间的通信,下面说说管道通信之无名管道。 1.概述   管道是Linux中很重要的一种通信方式,他是把一个程序的输出直接连接到另一个程序的输入,并且管道具有队列的特性。如Linux命令,“ps -ef | grep root”。
909 0
《UNIX网络编程 卷2:进程间通信(第2版)》——1.8 书中IPC例子索引表
生产者-消费者:一个或多个线程或进程(生产者)把数据放到一个共享缓冲区中,另有一个或多个线程或进程(消费者)对该共享缓冲区中的数据进行操作。序列号持续增1:一个或多个线程或进程给一个共享的序列号持续增1。该序列号有时在一个共享文件中,有时在共享内存区中。
1124 0
UNIX环境高级编程笔记之进程控制
  本章重点介绍了进程控制的几个函数:fork、exec族、_exit、wait和waitpid等,主要需要掌握的是父进程和子进程之间的运行机制,怎么处理进程的正常和异常终止、以及怎么让进程执行不同的程序等知识点。下一章将进一步说明一个进程和其他进程之间的关系——会话和作业控制。
702 0
15、深入理解计算机系统笔记:进程控制
1、获取进程ID[1] 每个进程都有一个唯一的正数(非0)进程ID(PID)。 示例代码 #include #include pid t getpid(void); pid t getppid(void); returns: PID of either the caller...
690 0
13692
文章
0
问答
文章排行榜
最热
最新
相关电子书
更多
JS零基础入门教程(上册)
立即下载
性能优化方法论
立即下载
手把手学习日志服务SLS,云启实验室实战指南
立即下载