进程和程序的区别
进程和程序的区别可以理解为,进程是活动的程序,而程序是一个代码的集合。进程是加载到内存中的程序,而程序没有加载到内存中,之在磁盘上保存着。下图是进程的结构,而程序进包含代码段
+-------------+
| 代码段 |
+-------------+
| 堆栈段 |
+-------------+
| 数据段 |
+-------------+
代码实例
fork.c :
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
int main()
{
pid_t pid;
char *message;
int n;
printf("fork program starting\n");
pid = fork();
switch(pid)
{
case -1:
perror("fork failed");
exit(1);
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);
}
exit(0);
}
(1) pid_t是进程号,是唯一表示进程的ID。
(2) pid_t fork(void) 函数
包含的头文件:
#include <sys/types.h>
#include <unistd.h>
调用fork可以创建一个全新的进程。这个系统调用对当前进程进行复制。在进程表里创建一个新的项目,新项目的许多属性与当前进程是相同的。新进程和原进程几乎一模一样,执行的也是相同的代码,但新进程有自己的数据空间、自己的环境等。
(3) 程序调用了fork函数的时候被分成了两个进程。在父进程里,fork函数返回新进程的PID进程号,新进程则返回0,这个可以做为区分父子进程的依据。
父进程和子进程的执行的代码都和fork.c里的代码一致。但是,fork根据不同进程返回不同的PID,那么父子进程的实际有效代码部分是不同的,下面只写实际有效的代码:
就是说,进程会根据PID的不同,有选择的执行各自的代码。
这个程序将产生两个进程,新进程(子进程)会输出消息5次,而父进程之输出3次。父进程会在子进程打印完它的全部消息之前退出。运行一下这个程序,我们可以看到如下交替输出的消息:
可以看到创建进程后,消息的输出是父子进程交替输出,且父进程在子进程之前结束。
wait()
如果要安排父进程在子进程结束之后才结束,则需要调用wait函数。
函数说明
pid_t wait(int * stat_loc)
包含的头文件:
#include <sys/types.h>
#include <sys/wait.h>
返回值:子进程的PID
参数:如果stat_loc不是一个空指针,状态信息将被写入它指向的位置
sys/wait.h文件中的状态信息见下表:
宏定义 说明
WIFEXITED(stat_val) 如果子进程正常结束,它就取一个非零值
WEXITSTATUS(stat_val) 如果WIFEXITED非零,它返回子进程的退出码
WIFSIGNALED(stat_val) 如果子进程因为一个未捕获的信号而终止,它就取一个非零值
WTERMSIG(stat_val) 如果WIFSIGNALED非零,它返回一个信号代码
WIFSTOPPED(stat_val) 如果子进程终止,它就取一个非零值
WSTOPSIG(stat_val) 如果WIFSTOPPED非零,它返回一个信号代码
wait系统调用会使父进程暂停执行,直到它的一个子进程结束为止。
代码实例:
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdio.h>
int main(){
pid_t pid;
char * message;
int n;
int exit_code;
printf("fork program starting\n");
pid = fork();
switch(pid){
case -1:
perror("fork failed");
exit(1);
case 0:
message ="This is the child";
n = 5;
/*子进程的退出码*/
exit_code = 37;
break;
default:
message = "This is the parent";
n = 3;
/*父进程的退出码*/
exit_code = 0;
break;
}
/*pid非0,在父进程执行*/
if(pid){
int stat_val;
pid_t child_pid;
/*父进程直到子进程推出后执行*/
child_pid = wait(&stat_val);
printf("Child process has finished: PID=%d\n",child_pid);
if(WIFEXITED(stat_val))
/*子进程正常结束,输出子进程退出码,即exit_code=37*/
printf("Child exited with code %d\n", WEXITSTATUS(stat_val));
else
/*子进程非正常结束*/
printf("Child terminated abnormally\n");
}
for(; n > 0; n--){
puts(message);
sleep(1);
}
exit(exit_code);
}
父进程通过wait系统调用把自己的执行挂起,直到子进程的状态信息出现为止。这将发生在子进程调用exit的时候;我们把它的退出码设置为37.
然后,父进程继续执行,通过测试wait调用的返回值确定子进程的已经正常结束,并从状态信息里提取出子进程的退出码。
运行效果见下图:
信号
信号是系统响应某些状况而产生的事件,进程在接受到信号时会采取相应的行动。信号可以明确地由一个进程产生发送到另外一个进程,用这种办法传递信息或协调操作行为。
进程可以生成信号、捕捉并相应信号或屏蔽信号。信号的名称是在头文件signal.h里定义。下面我列出一部分,如下:
信号名称 说明
SIGALRM 警告钟
SIGHUP 系统挂断
SIGINT 终端中断
SIGKILL 停止进程(此信号不能被忽略或捕获)
SIGPIPE 向没有读者的管道写数据
SIGQUIT 终端退出
SIGTERM 终止
如果进程接收到上表中的某个信号但实现并没有安排捕捉它,进程就会立刻终止。
函数
#include <signal.h>
void (*signal(int sig, void (*func) (int) ))(int);
第一个参数sig就是准备捕获或屏蔽的信号,接收到指定信号时将调用func函数处理。
实例1—处理SIGINT信号
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
void ouch(int sig)
{ /*此处,signal(SIGINT, SIG_DFL),SIG_DFL表示 ouch函数捕获到SIGINT信号,作出输出信息处理之后,恢复了SIGINT的默认行为*/
printf("OUCH! - I got signal %d\n", sig);
(void) signal(SIGINT, SIG_DFL);
}
int main()
{
(void) signal(SIGINT, ouch);
while(1) {
printf("Hello World!\n");
sleep(1);
}
}
这个程序就是截获组合键Ctrl+C产生的SIGINT信号。没有信号出现时,它每隔一秒就会输出一个消息。第一次按下Ctrl+C产生的SIGINT信号,程序会调用ouch函数,输出信息,同时,恢复SIGINT为默认行为(即按下Ctrl+C组合键后即结束运行),那么第二次按下Ctrl+C组合键时,程序就结束了运行。
实例2—模仿闹钟行为
使用到的函数:
函数1
#include <sys/types.h>
#include <signal.h>
int kill (pid_t pid, int sig);
kill函数的作用是把sig信号发送给标识为pid的进程,成功时返回“0”,失败时返回“-1”. 要想发送一个信号,两个进程(发送和接受两方)必须拥有同样的用户ID,,就是说,
你只能想自己的另一个进程发送信号。但是超级用户可以向任何进程发送信号。
函数2
#include <unistd.h>
unsigned int alarm (unsigned int seconds ) ;
alarm函数是在seconds秒后安排发送一个SIGALARM信号。若seconds为0,表示将取消全部已经设置的闹钟请求。每一个进程只有一个可用的闹钟。
它的返回值是前一个闹钟闹响之前还需经过的剩余秒数。调用失败则返回“-1”.
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
static int alarm_fired = 0;
void ding(int sig)
{
alarm_fired = 1;
}
int main()
{
int pid;
printf("闹钟程序已经启动\n");
/*子进程休眠5秒后向父进程发送SIGALARM信号,然后结束进程*/
if((pid = fork()) == 0) {
sleep(5);
kill(getppid(), SIGALRM);
exit(0);
}
/*父进程执行的内容*/
printf("5秒后闹铃启动\n");
(void) signal(SIGALRM, ding);
/*将运行的程序挂起,直到接收到信号为止*/
pause();
if (alarm_fired)
printf("Ding!\n");
printf("done\n");
exit(0);
}
程序通过fork启动一个新进程,这个紫禁城休眠5秒后向 自己的父进程发送一个SIGALARM信号。父进程在安排好捕捉SIGALARM信号后暂停运行,直到接收到一个信号为止。
运行结果见下图:
进程与线程
(1) 线程是进程的一个实体,是CPU调度和分派的基本单位,,它是比进程更小的能独立运行的基本单位.
(2) 进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。
下图是多线程的结构:
而进程之间的通信有两种方式,一种是在两个进程之间分配一个共享内存区域,另一种方法是通过内核来通信
_REENTRANT宏
在一个多线程程序里,默认情况下,只有一个errno变量供所有的线程共享。在一个线程准备获取刚才的错误代码时,该变量很容易被另一个线程中的函数调用所改变。类似的问题还存在于fputs之类的函数中,这些函数通常用一个单独的全局性区域来缓存输出数据。
为解决这个问题,需要使用可重入的例程。可重入代码可以被多次调用而仍然工作正常。编写的多线程程序,通过定义宏_REENTRANT来告诉编译器我们需要可重入功能,这个宏的定义必须出现于程序中的任何#include语句之前。
_REENTRANT为我们做三件事情:
(1)它会对部分函数重新定义它们的可安全重入的版本,这些函数名字一般不会发生改变,只是会在函数名后面添加_r字符串,如函数名gethostbyname变成gethostbyname_r。
(2)stdio.h中原来以宏的形式实现的一些函数将变成可安全重入函数。
(3)在error.h中定义的变量error现在将成为一个函数调用,它能够以一种安全的多线程方式来获取真正的errno的
基本函数
(1) pthread_create函数
#include <pthread.h>
int pthread_create ( pthread_t *thread, pthread_attr_t * attr, void* (*start_routine)(void*), void *arg );
返回值:调用成功返回“0”,如果失败则返回一个错误。
第一个参数:进程创建时,会分配一个唯一的PID标识,同样的,线程创建时,也会用一个指向pthread_t类型的数据类型作为新线程的标识.
第二个参数:对程序的属性进行设置
第三个参数:线程将要启动执行的函数,该函数的返回值和参数都是void指针,这样就可以传递任意类型的指针
第四个参数:传递给线程将要执行的函数(第三个参数)的参数
(2) pthread_exit函数
#include <pthread.h>
void pthread_exit (void * retval );
线程在结束时必须调用pthread_exit函数,这与一个进程在结束时要调用exit是同样的道理。
返回值:返回一个指向某个对象的指针,绝不要用它返回一个指向一个局部变量的指针,因为局部变量会在线程出现严重问题时消失得无影无踪。
(3) pthread_join函数
#include <pthread.h>
int pthread_join (pthread_t th, void ** thread_return );
pthread_join相当于进程用来等待子进程的wait函数,它的作用是在线程结束后把它们归并到一起。
返回值:成功时返回“0”, 失败时返回一个错误代码
第一个参数:将要等待的线程,它就是pthread_create返回的那个标识符
第二个参数:是一个指针,它指向另外一个指针,而这个指针指向线程的返回值
简单实例:
thread.c文件
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
void *thread_function(void*arg);
/*message是共享的数据*/
char message[] = "Hello World";
int main(){
int res;
pthread_t a_thread;
void *thread_result;
/*NULL表示不修改线程的属性,a_thread是新线程的标识符,以后的对新线程的引用就使用这个标识符*/
res = pthread_create(&a_thread, NULL, thread_function,(void*)message);
if(res != 0){
perror("Thread creation failed");
exit(EXIT_FAILURE);
}
printf("waiting for thread to finish...\n");
/*等待新线程执行完,然后合并新线程,thread_result是新线程的返回值,
这里是"Thank you for the CPU time",即pthread_exit的参数内容,这个函数会等到新线程结束后才返回*/
res = pthread_join(a_thread, &thread_result);
if(res != 0){
perror("THread join failed");
exit(EXIT_FAILURE);
}
printf("Thread joined, it returned %s\n",(char*)thread_result);
printf("Message is now %s\n", message);
exit(EXIT_SUCCESS);
}
void *thread_function(void* arg){
printf("thread_function is running. Argument was %s\n",(char*)arg);
sleep(3);
strcpy(message, "Bye");
pthread_exit("Thank you for the CPU time");
}
程序调用了pthread_create后,新线程开始执行。就是说,调用成功后,我们就有两个线程在运行。
原先的老线程将执行pthread_create后的代码,而新线程就去执行thread_function函数。
一开始,message是“Hello World”,但在新线程里,message被改成“Bye”。新线程结束后,输出的message依然是“Bye.”,因为message是共享的数据。
使用互斥量进行同步
互斥
简单地理解就是,一个线程进入工作区后,如果有其他线程想要进入工作区,它就会进入等待状态,要等待工作区内的线程结束后才可以进入。
基本函数
(1) pthread_mutex_init函数
原型:int pthread_mutex_init ( pthread_mutex_t *mutex, const pthread_mutexattr_t* attr);
描述:设置互斥量的属性
参数:第一个参数:预先声明的pthread_mutex_t对象指针
第二个参数:互斥锁属性,NULL表示使用默认属性
返回值:成功时返回0, 失败时返回一个错误代码
(2) pthread_mutex_lock函数
原型:int pthread_mutex_lock ( pthread_mutex_t *mutex );
描述:pthread_mutex_lock返回时,互斥锁被锁定,如果这个互斥锁被一个线程锁定和拥有,那么另一个线程要调用这 个函数会进入堵塞状态(即等待状态),直到互斥锁被释放为止。
返回值:成功时返回0, 失败时返回一个错误代码
(3) pthread_mutex_unlock函数
原型:int pthread_mutex_unlock ( pthread_mutex_t *mutex );
描述:释放互斥锁
返回值:成功时返回0, 失败时返回一个错误代码
(4) pthread_mutex_destroy函数
原型:int pthread_mutex_destroy ( pthread_mutex_t *mutex );
描述:删除互斥锁
返回值:成功时返回0, 失败时返回一个错误代码
实例
lock.c文件
描述:这个程序主要可以概括为主线程负责接受输入的字符串,而子线程则负责统计并输出字符数。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
#include <semaphore.h>
void *thread_function(void *arg);
pthread_mutex_t work_mutex;
#define WORK_SIZE 1024
char work_area[WORK_SIZE];
int time_to_exit = 0;
int main() {
int res;
pthread_t a_thread;
void *thread_result;
/*初始化互斥量*/
res = pthread_mutex_init(&work_mutex, NULL);
if (res != 0) {
perror("互斥量初始化失败!");
exit(EXIT_FAILURE);
}
/*启动新线程*/
res = pthread_create(&a_thread, NULL, thread_function, NULL);
if (res != 0) {
perror("线程创建失败");
exit(EXIT_FAILURE);
}
pthread_mutex_lock(&work_mutex);
printf("请输入一些文本内容. 输入“end”结束\n");
while(!time_to_exit) {
fgets(work_area, WORK_SIZE, stdin);
pthread_mutex_unlock(&work_mutex);
while(1) {
pthread_mutex_lock(&work_mutex);
/*统计字符工作未完成*/
if (work_area[0] != '\0') {
pthread_mutex_unlock(&work_mutex);
sleep(1);
}
else {
/*统计字符工作完成,跳出内层循环,重新读取输入*/
break;
}
}
}
pthread_mutex_unlock(&work_mutex);
printf("\n等待线程结束...\n");
res = pthread_join(a_thread, &thread_result);
if (res != 0) {
perror("Thread join failed");
exit(EXIT_FAILURE);
}
printf("Thread joined\n");
pthread_mutex_destroy(&work_mutex);
exit(EXIT_SUCCESS);
}
/*主线程首先锁定工作区,在获取输入的字符后,释放工作区,让其他线程对字符个数进行统计。work_area[0[为空字符时表示统计结束。通过周期性地对互斥量进行加锁,检查是否已经统计完。*/
/*在线程中要执行的代码*/
void *thread_function(void *arg) {
sleep(1);
pthread_mutex_lock(&work_mutex);
while(strncmp("end", work_area, 3) != 0) {
printf("你输入了 %d 个字符\n", strlen(work_area) -1);
work_area[0] = '\0';
pthread_mutex_unlock(&work_mutex);
sleep(1);
pthread_mutex_lock(&work_mutex);
while (work_area[0] == '\0' ) {
pthread_mutex_unlock(&work_mutex);
sleep(1);
pthread_mutex_lock(&work_mutex);
}
}
time_to_exit = 1;
work_area[0] = '\0';
pthread_mutex_unlock(&work_mutex);
pthread_exit(0);
}
在新线程一上来先试图对互斥量进行加锁。如果它已经被锁上,新线程就会进入堵塞状态知道互斥锁释放为止,一旦可以进入工作区,就先检查是否有退出请求(end)如果有,就设置time_to_exit变量和work_area,然后退出程序。
如果没有退出,那么就对字符个数进行统计。把work_area[0]设置为空,表示统计工作完成。接下来就释放互斥锁,等待主线程的运行,周期性地给互斥量加锁,如果加锁成功,就检查主线程是否又给我们新的字符串统计。如果没有,就释放互斥锁继续等待。