查看进程
Linux:使用ps命令
Windows:使用任务管理器
Linux创建进程
通过调用fork函数创建进程:
#include <unistd.h>
pid_t fork(void);
//成功时返回进程ID,失败时返回-1
fork函数将创建调用的进程副本
。也就是说,并非根据完全不同的程序创建进程,而是复制正在运行的、调用fork函数的进程。另外,两个进程都将执行fork函数调用后的语句(准确地说是在fork函数返回后)。但因为通过同一个进程、复制相同的内存空间,之后的程序l流要根据fork函数地返回值加以区分。即利用fork函数地如下特点区分程序执行流程。
- 父进程:
fork函数返回子进程ID
。 - 子进程:
fork函数返回0
。
注意:如果父进程不等待子进程的结束而先结束,则子进程不受父进程影响,换句话说就是父进程和子进程各不影响。
代码示例
#include <stdio.h>
#include <unistd.h>
int gval = 10;
int main(int argc,char *argv[])
{
pid_t pid;
int lval = 20;
gval++,lval+=5;
pid = fork();
if(pid == 0) //if Child Process
gval += 2,lval += 2;
else //if Parent Process
gval -= 2,lval -= 2;
if(pid == 0) //if Child Process
printf("Child proc:[%d %d]\n",gval,lval);
else //if Parent Process
printf("Parent proc:[%d %d]\n",gval,lval);
return 0;
}
输出:
僵尸进程
进程完成工作后(执行完main函数中的程序后)应被销毁,但有时这些进程将变成僵尸进程,占用系统中的重要资源。
产生僵尸进程的原因
为了防止僵尸进程的产生,先解释产生僵尸进程的原因。利用如下两个示例展示调用fork函数产生子进程的终止方式。
- 传递参数并调用exit函数。
- main函数中执行return语句并返回值。
向exit函数传递的参数值和main函数的return语句返回的值都会传递给操作系统。而操作系统不会销毁子进程,直到把这些值传递给产生该子进程的父进程。处在这种状态下的进程就是僵尸进程。也就是说,将子进程变成僵尸进程的正是操作系统。
既然如此,此僵尸进程何时被销毁呢?其实已经给出提示。
“应该向创建子进程的父进程传递子进程的exit参数值或return语句的返回值。”
如何向父进程传递这些值呢?操作系统不会主动把这些值传递给父进程。只有父进程主动发起请求(函数调用)时,操作系统才会传递该值。换言之,如果父进程未主动要求获得子进程的结束状态值,操作系统将一直保存,并让子进程长时间处于僵尸进程状态。也就是说,父母要负责收回自己生的孩子(也许这种描述有些不妥)。
创建僵尸进程示例
#include <stdio.h>
#include <unistd.h>
int main(int argc,char *argv[])
{
pid_t pid = fork();
if(pid == 0) //if Child Process
{
puts("Hi,I am a child process");
}else{
printf("Child Process ID:%d\n",pid);
sleep(30); //Sleep 30 sec.
}
if(pid == 0)
{
puts("End child process");
}else{
printf("End parent process");
}
return 0;
}
销毁僵尸进程1:利用wait函数
#include <sys/wait.h>
pid_t wait(int * statloc);
//成功时返回终止的子进程ID,失败时返回-1。
调用此函数时如果已有子进程终止,那么子进程终止时传递的返回值( exit函数的参数值、main函数的return返回值)将保存到该函数的参数所指内存空间。但函数参数指向的单元中还包含其他信息,因此需要通过下列宏进行分离。
- WIFEXITED子进程正常终止时返回“真”( true )。
- WEXITSTATUS返回子进程的返回值。
也就是说,向wait函数传递变量status的地址时,调用wait函数后应编写如下代码。
if(WIFEXITED(status))//是正常终止的吗?
puts ( "Normal termination!");
printf( "Child pass num: %d", WEXITSTATUS(status));1/那么返回值是多少?
}
利用wait函数销毁进程示例
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main(int argc,char * argv[])
{
int status;
pid_t pid = fork();
if(pid == 0)
{
return 3;
}
else{
printf("Child PID:%d\n",pid);
pid = fork();
if(pid == 0)
{
exit(7);
}
else{
printf("Child PID:%d\n",pid);
wait(&status);
if(WIFEXITED(status))
printf("Child send one:%d\n",WEXITSTATUS(status));
wait(&status);
if(WIFEXITED(status))
printf("Child send two:%d\n",WEXITSTATUS(status));
sleep(10);
}
}
return 0;
}
注意:调用wait函数时,如果没有已终止的子进程,那么程序将阻塞(Blocking)直到有子进程终止,因此需谨慎调用该函数。
调用waitpid函数程序不会阻塞。
销毁僵尸进程2:使用waitpid函数
#include <sys/wait.h>
pid_t waitpid(pid_t pid,int * statloc,int options);
//成功时返回终止的子进程ID(或0),失败时返回-1。
- pid:等待终止的目标子进程的ID,若传递-1,则与wait函数相同,可以等待任意子进程终止。
- statloc:与wait函数的statloc参数具有相同含义。
- options:传递头文件sys/wait.h中声明的常量WNOHANG,即使没有终止的子进程也不会进入阻塞状态,而是返回0并退出函数。
利用waitpid函数销毁进程示例
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main(int argc,char * argv[])
{
int status;
pid_t pid = fork();
if(pid == 0)
{
sleep(15);
return 24;
}else{
while(!waitpid(-1,&status,WNOHANG))
{
sleep(3);
puts("sleep 3sec.");
}
if(WIFEXITED(status))
printf("Child send %d\n",WEXITSTATUS(status));
}
return 0;
}
输出:
信号处理
用于接收子进程终止的相关事宜。
“嘿,父进程!你创建的子进程终止了!”
信号与signed函数
#include <signal.h>
void (*signal(int signo,void (*func)(int)))(int);
//为了在产生信号时调用,返回之前注册的函数指针
- 函数名:signal
- 参数:int signo, void (* func)(int)
- 返回类型:参数为int型,返回void型函数指针。
调用上述函数时,第一个参数为特殊情况信息,第二个参数为特殊情况下将要调用的函数的地址值(指针)。发生第一个参数代表的情况时,调用弟二个参数所指的函数。下面给出可以在signal函数中注册的部分特殊情况和对应的常数。
- SIGALRM:已到通过调用alarm函数注册的时间。
- SIGINT:输入CTRL+C。
- SIGCHLD:子进程终止。
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
//返回0或以秒为单位的距SIGALRM信号发生所剩余时间
如果调用该函数的同时向它传递一个正整型参数,相应时间后(以秒为单位)将产生SIGALRM信号。若向该函数传递0,则之前对SIGALRM信号的预约将取消。如果通过该函数预约信号后未指定该信号对应的处理函数,则(通过调用signal函数)终止进程,不做任何处理。希望引起注意。
signed函数信号处理示例
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void timeout(int sig)
{
if(sig == SIGALRM)
puts("Time out!");
alarm(2);
}
void keycontrol(int sig)
{
if(sig == SIGALRM)
puts("CTRL+C pressed");
}
int main(int argc,char *argv[])
{
int i;
signal(SIGALRM,timeout);
signal(SIGINT,keycontrol);
alarm(2);
for(i = 0;i < 3;i++)
{
puts("wait...");
sleep(100);
}
return 0;
}
输出:
进程处于睡眠状态时无法调用函数。因此,产生信号时,为了调用信号处理器,将唤醒由于调用sleep函数而进入阻塞状态的进程。而且,进程一旦被唤醒,就不会再进人睡眠状态。即使还未到sleep函数中规定的时间也是如此。
所以,上述示例运行不到10秒就会结束,连续输入CTRL+C则有可能1秒都不到。
利用sigaction函数进行信号处理
优势:signal函数在UNIX系列的不同操作系统中可能存在区别,但sigaction函数完全相同。
实际上现在很少使用signal函数编写程序,它只是为了保持对旧程序的兼容。
#include <signal.h>
int sigaction(int signo,const struct sigaction* act,struct sigaction*oldact);
//成功时返回0,失败时返回-1。
- signo:与signal函数相同,传递信号信息。
- act:对应于第一个参数的信号处理函数(信号处理器)信息。
- oldact:通过此参数获取之前注册的信号处理函数指针,若不需要则传递0。
声明并初始化sigaction结构体变量以调用上述函数,该结构体定义如下。
struct sigaction
void (*sa_handler)(int);sigset_t sa_mask;
int sa_flags;
}
此结构体的sa_handler成员保存信号处理函数的指针值(地址值)。sa_mask和sa_flags的所有位均初始化为0即可。这2个成员用于指定信号相关的选项和特性,而我们的目的主要是防止产生僵尸进程,故省略。
利用sigaction信号处理示例
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void timeout(int sig)
{
if(sig == SIGALRM)
puts("Time out!");
alarm(2);
}
int main(int argc,char *argv[])
{
int i;
struct sigaction act;
act.sa_handler = timeout;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
sigaction(SIGALRM,&act,0);
alarm(2);
for(i = 0; i < 3; i++)
{
puts("wait...");
sleep(100);
}
return 0;
}
输出:
分割TCP的I/O程序
分割TCP的I/O程序的原因有很多,但最重要的一点是,程序的实现更加简单。按照这种实现方式,父进程只需编写接收数据的代码,子进程中只需编写发送数据的代码,所以会简化。
进程通信
进程通信意味着两个不同进程间可以交换数据。
常见进程的通信方式:
- 管道pipe:管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。
- 命名管道FIFO:有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。
- 消息队列MessageQueue:消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
- 共享存储SharedMemory:共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量,配合使用,来实现进程间的同步和通信。
- 信号(signal) : 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。
- 信号量Semaphore:信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
- 套接字Socket:套解口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同及其间的进程通信。
通过管道实现进程间通信
#include <unistd.h>
int pipe(int filedes[2]);
//成功时返回0,失败时返回-1。
- filedes[0]:通过管道
接收
数据时使用的文件描述符,即管道出口。 - filedes[1]:通过管道
传输
数据时使用的文件描述符,即管道入口。
管道通信示例
#include <stdio.h>
#include <unistd.h>
#define BUF_SIZE 30
int main(int argc,char * argv[])
{
int fds[2];
char str[] = "Who are you?";
char buf[BUF_SIZE];
pid_t pid;
pipe(fds);
pid = fork();
if(pid == 0)
{
write(fds[1],str,sizeof(str));
}else{
read(fds[0],buf,BUF_SIZE);
puts(buf);
}
return 0;
}
说明:其实可以通过一个管道进行双向通信,但是数据进入管道后成为了无主数据。也就是通过read函数先读取数据的进程将得到数据,即使该进程将数据传到了管道,可以解决的办法是:创建2个管道
。