现在的位置: 首页 > 综合 > 正文

Linux程序设计——进程和信号

2013年10月20日 ⁄ 综合 ⁄ 共 3458字 ⁄ 字号 评论关闭

进程和信号在Linux系统中无处不在,它掌控着Linxu的活动,确保系统的正常运行,其重要性不言而喻。

1、进程

进程是指运行着一个或多个线程的地址空间和这些线程所需要的系统资源。在Linux中每个正在运行的程序实例都可以被看作是进程,这些进程之间可以共享程序代码和系统函数库,因此在任何时刻系统内存中只有代码的一份副本。

系统中的每个进程在创建时都会被分配一个唯一的数字编号,称之为进程标识符或PID。在Linux系统中,PID的取值范围是2~32768。之所以是从2开始,是因为Linux将1保留分配给特殊进程init,这个进程主要用来管理其他进程,可以通过运行ps命令查看系统当前正在运行的进程信息,包括进程的ID、进程的状态、命令字符串等。

上面提到在系统中正在运行的程序实例都可以被看作进程,意味着同一时刻系统中维持多个进程的运行。但是系统硬件资源是有限的,因此需要合理地进程调度,实现这个功能的是LInux内核的进程调度器。进程调度器根据进程的优先级决定下一个时间片分配给哪个进程。在Linux中,任何进程的运行时间不可能超过分配给它们的时间片,它们采用的是抢先式多任务处理,因此进程的挂起和继续运行无需彼此之间的协作。在系统中,长期不间断运行的程序的优先级一般会比较低,相反等待时间越长的进程的优先级越高,继续运行的概率也高。但是进程的优先级并不是完全由系统控制的,可以通过程序调整程序的优先级。比如需要立即运行的进程,我们可以提高其优先级,保证它能够获取下一个时间片。

1)新建进程

创建新进程可以调用系统方法system(const char *string),参数代表启动进程的命令。但是system对shell的依赖性比较大,因为它必须用一个shell来启动需要的程序,所以使用比较多的方法是exec和fork。exec比system函数更有效在于它启动新的程序后,原来的程序就不再运行,而且由exec启动的新进程继承了原进程的很多特性。另外一个方法就是fork,这个系统调用复制当前进程,在进程表中创建一个新的表项,新表项中的许多属性与当前进程是相同的,但新进程有自己的数据空间、环境和文件描述符。调用fork创建新进程时,在父进程中的fork调用返回的是新的子进程的PID,子进程中的fork调用返回0。父进程可以通过返回值判断哪些是子进程。

#include <stdio.h>
#include <stdlib.h>

int main()
{
	pid_t pid;
	char *message;
	int n;
	printf("fork program starting\n");
	pid = fork();
	switch(pid)
	{
	case -1:
		perror("fork failed\n");
		break;
	case 0:
		message = "This is the child";
		n = 5;
		break;
	default:
		message = "This is the parent";
		n = 3;
		break;
	}
	for(; n > 0; n--)
	{
		puts(message);
		sleep(1);
	}

	return 0;
}


2)等待进程

当用fork启动一个子进程时,子进程有自己的生命周期并将独立运行。如果父进程需要知道子进程何时运行结束,就可以通过在父进程中调用wait函数让父进程等待子进程结束。wait系统调用将暂停父进程直到它的子进程结束为止,返回子进程的PID,通常是已经运行结束的进程的PID。通过wait系统调用还可以获取子进程的退出状态,即子进程的main函数返回的值或子进程中exit函数的退出码。

#include <stdio.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <stdlib.h>

int main()
{
	pid_t pid;
	char *message;
	int n;
	int exit_code;
	printf("fork programming starting\n");
	pid = fork();
	switch(pid)
	{
	case -1:
		perror("fork failed\n");
		exit(1);
	case 0:
		message = "This is child";
		n = 5;
		exit_code = 37;
		break;
	default:
		message = "This is parent";
		n = 3;
		exit_code = 0;
		break;
	}
	for(; n > 0; n--)
	{
		puts(message);
		sleep(1);
	}
	if(pid != 0)
	{
		int start_val;
		pid_t child_pid;
		child_pid = wait(&start_val);
		printf("Child has finished: PID = %d\n", child_pid);
		if(WIFEXITED(start_val))
		{
			printf("Child exited with code: %d\n", WEXITSTATUS(start_val));
		}
		else
		{
			printf("Child terminated abnormally\n");
		}
	}
	exit(exit_code);
}

3)僵尸进程

子进程终止时,它与父进程之间的关联还会保持,直到父进程也正常终止或父进程调用wait才结束。因此进程表中代表子进程的表项不会立即释放。虽然子进程已经不再运行,但它仍然存在于系统中,因此它的退出码还需要保存起来,以备父进程今后的wait调用使用。这时子进程将成为一个死(defunct)进程或僵尸(zombie)进程。僵尸进程会带来系统资源的浪费,可以通过下面几种方法避免僵尸进程:

a、父进程通过wait和waitpid等函数等待子进程结束,这会导致父进程挂起。

b. 如果父进程很忙,那么可以用signal函数为SIGCHLD安装handler,因为子进程结束后, 父进程会收到该信号,可以在handler中调用wait回收。

c、如果父进程不关心子进程什么时候结束,那么可以用signal(SIGCHLD,
SIG_IGN) 通知内核,自己对子进程的结束不感兴趣,那么子进程结束后,内核会回收, 并不再给父进程发送信号。

d、fork两次,父进程fork一个子进程,然后继续工作,子进程fork一
个孙进程后退出,那么孙进程被init接管,孙进程结束后,init会回收。不过子进程的回收还要自己做。

2、信号

信号是UNIX和Linux系统响应某些条件而产生的一个事件。信号是由于某些错误条件如内存段冲突、浮点运算错误或非法指令等造成的。它们由shell和终端处理器生成来引起中断,还可以作为在进程间传递消息或修改行为的一种方式,明确地由一个进程发送给另一个进程。信号可以被生成、捕获、响应或忽略。信号的名称定义在signal.h中,以SIG开头。如果进程接收到某个信号,但事先没有安排捕获它,进程将会立刻终止。

对信号处理的函数为signal,但是这个函数在用户想要保留信号处理函数时存在问题。程序第一次接收到信号到信号处理函数重建这段时间内如果有程序接收到第二个信号,第二次的信号是得不到处理的。sigaction可以解决这个问题,它可以连续处理到来的信号。

另外使用信号并挂起程序的执行是Linux程序设计很重要的一部分,可以保证程序不需要问题在执行,它可以被挂起等待直到某个事件发生后再继续运行。常用的程序挂起方法有pause系统调用和sigsuspend系统调用,进程可以通过调用sigsuspend函数挂起自己的执行,直到信号集中的一个信号到达为止。

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>

void ouch(int sig) // 信号处理函数
{
	printf("OUCH! - I got signal %d\n", sig);
}

int main()
{
	struct sigaction act;
	act.sa_handler = ouch;
	act.sa_flags = 0;
	sigemptyset(&act.sa_mask);

	sigaction(SIGINT, &act, 0); // 截获SIGINT信号
	while(1)  // 程序将一直打印Hello world直到按下Ctrl+\组合键
	{
		printf("Hello world\n");
		sleep(1);
	}

	return 0;
}


和进程、信号相关的另外一个重要的概念是线程,线程比进程更加复杂,在后面的博文中将会详细介绍。

抱歉!评论已关闭.