拥有2个运算器的CPU称作双核CPU,拥有4个运算器的CPU称作4核CPU。核的个数与可同时运行的进程数相同。
通过fork函数创建进程
fork函数创建调用的进程副本,两个进程都将执行fork函数返回后的语句。
#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)
printf("Child Proc: [%d, %d] \n", gval, lval);
else
printf("Parent Proc: [%d, %d] \n", gval, lval);
return 0;
}
进程和僵尸进程
一个已经终止但是其父进程尚未对其进行善后处理(获取终止子进程的有关信息,释放它仍占用的资源)的进程称为僵尸进程(zombie)。
产生僵尸进程的原因
向exit函数传递的参数值和main函数的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(100); // Sleep 30 sec.
}
if(pid==0)
puts("End child process");
else
puts("End parent process");
return 0;
}
父进程暂停30秒,如果父进程终止,处于僵尸状态的子进程将同时被销毁。
销毁僵尸进程1:利用wait函数
//wait.c
#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); //将之间终止的子进程信息保存到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(30); // Sleep 30 sec.
}
}
return 0;
}
调用wait函数时,如果没有已终止的子进程,那么程序将阻塞直到有子进程终止,因此需谨慎调用该函数。
销毁僵尸进程2:使用waitpid函数
更多参数相关细节可参考《unix环境高级编程》
#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;
}
信号处理
此处的“信号”是在特定事件发生时由操作系统向进程发送的消息。为了响应该消息,执行与消息相关的自定义操作的过程称为“处理”或“信号处理”。
信号和signal函数
调用该函数时,第一个参数为特殊情况信息,第二个参数为特殊情况下将要调用的函数的地址值(指针),第二个参数的int形参就是收到的信号值。发生第一个参数代表的情况时,调用第二个参数所指的函数。
signal函数中注册的部分特殊情况和对应的常数:
alarm函数:
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void timeout(int sig) //信号处理函数(信号处理器)
{
if(sig==SIGALRM)
puts("Time out!");
alarm(10);
}
void keycontrol(int sig)
{
if(sig==SIGINT)
puts("CTRL+C pressed");
}
int main(int argc, char *argv[])
{
int i;
signal(SIGALRM, timeout);
signal(SIGINT, keycontrol);
alarm(10);
for(i=0; i<3; i++)
{
puts("wait...");
sleep(100);
}
return 0;
}
产生信号时,为了调用信号处理器,将唤醒由于调用sleep函数而进入阻塞状态的进程。而且,进程一旦被唤醒,就不会再进入(由上一次调用sleep导致的)睡眠状态。
利用sigaction函数进行信号处理
sigaction函数完全可以替代signal函数,而且更稳定,因为signal函数在UNIX的不同操作系统中可能存在区别,但sigaction函数完全相同。
sa_handler成员保存信号处理函数的指针值(地址值)。sa_mask和sa_flags用于指定信号相关的选项和特性。(我看网上对这个结构体的定义与书上的不同,先在这儿做个注释,书上后续的内容可能会有补充)
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;
//int sigemptyset(sigset_t *set) 将信号集初始化为空
sigemptyset(&act.sa_mask); //将sa_mask成员的所有位初始化为0
act.sa_flags=0;
sigaction(SIGALRM, &act, 0);
alarm(2);
for(i=0; i<3; i++)
{
puts("wait...");
sleep(100);
}
return 0;
}
利用信号处理技术消灭僵尸进程
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
void read_childproc(int sig)
{
int status;
pid_t id=waitpid(-1, &status, WNOHANG);
if(WIFEXITED(status))
{
printf("Removed proc id: %d \n", id);
printf("Child send: %d \n", WEXITSTATUS(status));
}
}
int main(int argc, char *argv[])
{
pid_t pid;
struct sigaction act;
act.sa_handler=read_childproc;
sigemptyset(&act.sa_mask);
act.sa_flags=0;
sigaction(SIGCHLD, &act, 0);
pid=fork();
if(pid==0)
{
puts("Hi! I'm child process");
sleep(10);
return 12;
}
else
{
printf("Child proc id: %d \n", pid);
pid=fork();
if(pid==0)
{
puts("Hi! I'm child process");
sleep(10);
exit(24);
}
else
{
int i;
printf("Child proc id: %d \n", pid);
for(i=0; i<5; i++)
{
puts("wait...");
sleep(5);
}
}
}
return 0;
}
基于多任务的并发服务器
每当有客户端请求服务时(连接请求),服务器端都创建子进程以提供服务。
调用fork函数后,父进程将文件描述符复制给子进程,而不是套接字。套接字并非进程所有,而是属于操作系统,只是进程拥有代表相应套接字的文件描述符。只有当套接字对应的文件描述符都销毁后,才能销毁套接字。因此,调用fork函数后,要将无关的套接字文件描述符关掉。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 30
void error_handling(char *message);
void read_childproc(int sig);
int main(int argc, char *argv[])
{
int serv_sock, clnt_sock;
struct sockaddr_in serv_adr, clnt_adr;
pid_t pid;
struct sigaction act;
socklen_t adr_sz;
int str_len, state;
char buf[BUF_SIZE];
if(argc!=2) {
printf("Usage : %s <port>\n", argv[0]);
exit(1);
}
act.sa_handler=read_childproc; //设置信号处理函数
sigemptyset(&act.sa_mask);
act.sa_flags=0;
state=sigaction(SIGCHLD, &act, 0);
serv_sock=socket(PF_INET, SOCK_STREAM, 0);
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family=AF_INET;
serv_adr.sin_addr.s_addr=htonl(INADDR_ANY);
serv_adr.sin_port=htons(atoi(argv[1]));
if(bind(serv_sock, (struct sockaddr*) &serv_adr, sizeof(serv_adr))==-1)
error_handling("bind() error");
if(listen(serv_sock, 5)==-1)
error_handling("listen() error");
while(1)
{
adr_sz=sizeof(clnt_adr);
clnt_sock=accept(serv_sock, (struct sockaddr*)&clnt_adr, &adr_sz);
if(clnt_sock==-1)
continue;
else
puts("new client connected...");
pid=fork(); //有新的连接请求时创建子进程
if(pid==-1)
{
close(clnt_sock);
continue;
}
if(pid==0)
{
close(serv_sock); //关闭父进程传递的用于监听的文件描述符
while((str_len=read(clnt_sock, buf, BUF_SIZE))!=0)
write(clnt_sock, buf, str_len);
close(clnt_sock);
puts("client disconnected...");
return 0;
}
else
close(clnt_sock); //关闭传递给子进程的文件描述符
}
close(serv_sock);
return 0;
}
void read_childproc(int sig)
{
pid_t pid;
int status;
pid=waitpid(-1, &status, WNOHANG);
printf("removed proc id: %d \n", pid);
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
分割TCP的I/O程序
分割I/O程序优点:1.程序实现更加简单,读写可以分别在父进程与子进程中实现
2.提高频繁交换数据的程序性能
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 30
void error_handling(char *message);
void read_routine(int sock, char *buf);
void write_routine(int sock, char *buf);
int main(int argc, char *argv[])
{
int sock;
pid_t pid;
char buf[BUF_SIZE];
struct sockaddr_in serv_adr;
if(argc!=3) {
printf("Usage : %s <IP> <port>\n", argv[0]);
exit(1);
}
sock=socket(PF_INET, SOCK_STREAM, 0);
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family=AF_INET;
serv_adr.sin_addr.s_addr=inet_addr(argv[1]);
serv_adr.sin_port=htons(atoi(argv[2]));
if(connect(sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr))==-1)
error_handling("connect() error!");
pid=fork();
if(pid==0)
write_routine(sock, buf);
else
read_routine(sock, buf);
close(sock);
return 0;
}
void read_routine(int sock, char *buf)
{
while(1)
{
int str_len=read(sock, buf, BUF_SIZE);
if(str_len==0)
return;
buf[str_len]=0;
printf("Message from server: %s", buf);
}
}
void write_routine(int sock, char *buf)
{
while(1)
{
fgets(buf, BUF_SIZE, stdin);
if(!strcmp(buf,"q\n") || !strcmp(buf,"Q\n"))
{
shutdown(sock, SHUT_WR); //通过shutdown向服务器发送
return;
}
write(sock, buf, strlen(buf));
}
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
close和shutdown的具体细节可以参考这篇文章,写的很好:tcp - close和shutdown关闭TCP连接_个人文章 - SegmentFault 思否