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,如需转载请自行联系原作者

相关文章
|
5天前
|
安全 Java 数据处理
Python网络编程基础(Socket编程)多线程/多进程服务器编程
【4月更文挑战第11天】在网络编程中,随着客户端数量的增加,服务器的处理能力成为了一个重要的考量因素。为了处理多个客户端的并发请求,我们通常需要采用多线程或多进程的方式。在本章中,我们将探讨多线程/多进程服务器编程的概念,并通过一个多线程服务器的示例来演示其实现。
|
7天前
|
安全 Java 开发者
深入理解Java并发编程:线程安全与性能优化
【4月更文挑战第9天】本文将深入探讨Java并发编程的核心概念,包括线程安全和性能优化。我们将详细解析Java中的同步机制,包括synchronized关键字、Lock接口以及并发集合等,并探讨它们如何影响程序的性能。此外,我们还将讨论Java内存模型,以及它如何影响并发程序的行为。最后,我们将提供一些实用的并发编程技巧和最佳实践,帮助开发者编写出既线程安全又高效的Java程序。
20 3
|
10天前
|
Java 调度
Java并发编程:深入理解线程池的原理与实践
【4月更文挑战第6天】本文将深入探讨Java并发编程中的重要概念——线程池。我们将从线程池的基本原理入手,逐步解析其工作过程,以及如何在实际开发中合理使用线程池以提高程序性能。同时,我们还将关注线程池的一些高级特性,如自定义线程工厂、拒绝策略等,以帮助读者更好地掌握线程池的使用技巧。
|
20天前
|
消息中间件 安全 Linux
线程同步与IPC:单进程多线程环境下的选择与权衡
线程同步与IPC:单进程多线程环境下的选择与权衡
57 0
|
21天前
|
消息中间件 存储 算法
【软件设计师备考 专题 】操作系统的内核(中断控制)、进程、线程概念
【软件设计师备考 专题 】操作系统的内核(中断控制)、进程、线程概念
66 0
|
10天前
|
设计模式 安全 Java
Java并发编程实战:使用synchronized关键字实现线程安全
【4月更文挑战第6天】Java中的`synchronized`关键字用于处理多线程并发,确保共享资源的线程安全。它可以修饰方法或代码块,实现互斥访问。当用于方法时,锁定对象实例或类对象;用于代码块时,锁定指定对象。过度使用可能导致性能问题,应注意避免锁持有时间过长、死锁,并考虑使用`java.util.concurrent`包中的高级工具。正确理解和使用`synchronized`是编写线程安全程序的关键。
|
12天前
|
Java
深入理解Java并发编程:线程池的应用与优化
【4月更文挑战第3天】 在Java并发编程中,线程池是一种重要的资源管理工具,它能有效地控制和管理线程的数量,提高系统性能。本文将深入探讨Java线程池的工作原理、应用场景以及优化策略,帮助读者更好地理解和应用线程池。
|
22天前
|
安全 Python
Python中的并发编程:多线程与多进程技术探究
本文将深入探讨Python中的并发编程技术,重点介绍多线程和多进程两种并发处理方式的原理、应用场景及优缺点,并结合实例分析如何在Python中实现并发编程,以提高程序的性能和效率。
|
8天前
|
Java
Java 并发编程:深入理解线程池
【4月更文挑战第8天】本文将深入探讨 Java 中的线程池技术,包括其工作原理、优势以及如何使用。线程池是 Java 并发编程的重要工具,它可以有效地管理和控制线程的执行,提高系统性能。通过本文的学习,读者将对线程池有更深入的理解,并能在实际开发中灵活运用。
|
4天前
|
安全 算法 Java
深入理解Java并发编程:线程安全与性能优化
【4月更文挑战第11天】 在Java中,高效的并发编程是提升应用性能和响应能力的关键。本文将探讨Java并发的核心概念,包括线程安全、锁机制、线程池以及并发集合等,同时提供实用的编程技巧和最佳实践,帮助开发者在保证线程安全的前提下,优化程序性能。我们将通过分析常见的并发问题,如竞态条件、死锁,以及如何利用现代Java并发工具来避免这些问题,从而构建更加健壮和高效的多线程应用程序。

相关实验场景

更多