进程
占用内存空间的正在运行的程序(躺在硬盘里的是程序,被加载器加载到内存中运行后,正在运行的是进程)
补充:拥有2个运算设备的CPU称作双核CPU,拥有4个运算器的CPU称作4核。核的个数与可同时运行的进程数相同。相反,若进程数超过核数,进程将分时使用CPU资源。只是因为CPU运行速度极快,我们会感觉所有进程同时运行而已(宏观上并行,微观上串行)。
进程ID
所有进程在创建时,操作系统都会为其分配一个ID号,其值是大于2的整数,ID=1的进程是操作系统启动后(用于协助操作系统)的首个进程,这个进程用户无法得到。命令ps查看当前运行的所有进程,ps命令详解: http://blog.youkuaiyun.com/lsbhjshyn/article/details/18549869
Linux下进程创建函数
pid_t fork(void); 成功返回进程ID,失败时返回-1
Fork函数被调用后,将会返回两次,返回值有三种情况:
1、返回-1,函数调用失败
2、返回值大于0,返回值为子进程ID,当前进程为父进程
3、返回值等于0,当前进程为子进程
子进程是调用fork函数的进程副本,子进程复制了父进程的内存空间,即他们有相同的代码段,数据段等(虽然拥有同样的代码段与数据段。但是子进程与父进程之间拥有完全独立的内存结构)。可以通过fork函数的返回值判断当前的进程是父进程还是子进程从而执行各自的动作。
僵尸进程
子进程的主函数函数体执行完毕,即进程结束,父进程存在但并未调用调用wait()或waitpid()函数来得到子进程的执行结果并清理task_struct,此种状态下的进程被称为僵尸进程
注:
1、调用exit函数,main函数中的retuen语句返回的参数值会传递给操作系统,操作系统并不会主动将子进程的返回值传递给其父进程,除非父进程调用wait()或waitpid函数主动发起请求,否则子进程将一直是僵尸进程,除非父进程结束,僵尸进程被init 进程接管,init进程会以父进程的身份对处于僵尸状态的子进程进行处理(父进程负责回收其子进程)
2、子进程结束时,其所使用的资源(文件描述符、内存等)都会被释放。但是为了将其执行结果有机会得以告诉父进程,task_struct会被保留,当父进程调用调用wait()或waitpid()函数来得到子进程的执行结果并清理task_struct后,僵尸进程彻底消失
3、孤儿进程,父进程先于子进程结束,linux系统规定:所有的孤儿进程都自动成为一个特殊进程(进程1,也就是init进程)的子进程。
销毁僵尸进程
父进程需要调用wait()函数或waitpid()函数获取子进程的返回值,并帮子进程”收尸”
wait()函数:
pid_t wait(int *statloc); //成功返回已结束的子进程的pid,失败返回-1(没有子进程结束)
参数statloc指向的内存空间保存了子进程结束时传递的返回值(exit()或主函数main()所返回)
传参时传递一个int型变量的地址即可
使用下面的宏宏可获取子进程结束的具体情况:
wait获取staus后检测处理
宏定义 描述
WIFEXITED(status) 如果进程子进程正常结束,返回一个非零值
WEXITSTATUS(status) 如果WIFEXITED非零,此宏返回子进程退出的返回值
WIFSIGNALED(status) 如果子进程因为捕获信号而终止,返回非零值
WTERMSIG(status) 如果WIFSIGNALED非零,使用此宏获得使子进程结束的信号编号
WIFSTOPPED(status) 如果进程被暂停,返回一个非零值
WSTOPSIG(status)如果WIFSTOPPED非零,使用此宏获得使子进程暂停的信号编号
WIFCONTINUED(status)如果进程暂停后继续运行,返回非零值
宏的参数为wait()函数指向的变量
示例:
int status;
pid_t pid = fork();
if(pid == 0)
{
exit(1); //或者return 0;
}
else
{
wait(&status); //成功时返回终止的子进程ID,失败时返回-1
if(WIFEXITED(status))//宏,子进程正常终止返回真
printf("Child return: %d \n", WEXITSTATUS(status)); //宏,返回子进程的返回值
}
注:wait()函数被调用后,要是没有已结束的子进程,wai()函数将发生阻塞,直到有子进程结束
waitpid()函数
pid_t waitpid(pid_t pid, int *statloc, int options); //成功返回已结束的子进程的ID或0,失败返回-1
参数一为等待结束的目标子进程的ID,传递-1则表示等待任意子进程结束
参数二同上述wait()函数
参数三传递常量WNOHANG时,如果没有结束的子进程也不会进入阻塞状态,将返回0
示例:int status;
pid_t pid = fork();
if(pid == 0)
{
sleep(15);
return 24;
}
else
{
//没有终止子进程返回0,有则返回终止子进程ID,失败时返回-1
while(!waitpid(-1, &status, WNOHANG))//不会阻塞
{
sleep(3);
puts("sleep 3s");
}
if(WIFEXITED(status))
printf("Child return %d \n", WEXITSTATUS(status));
}
信号与信号处理
子进程也有很多事要做,也很忙,不可能仅仅为了回收子进程而毫无意义的等待,而且这个等待还是没有固定期限的,应该使用信号处理的机制,即向操作系统注册回调函数,当子进程结束时,系统执行回调函数清理子进程
信号:在特定事件发生时由操作系统向进程发送的消息
信号处理:未响应操作系统的消息而执行的与消息相关的自定义操作的过程
signal函数
void (*signal(int signo, void(* handler)(int)))(int); //返回指向参数为int返回值为void的类型的函数指针
参数一:信号名
参数二:指向信号处理函数的函数指针
第一个参数的信号发生时,操作系统会调用第二个参数指向的信号处理函数
第一个参数一般需要用到:
SIGALRM 通过alarm函数注册的时间到
SIGINT 发生CTRL + C
SIGCHLD 子进程结束
注:信号处理函数类型必须与signal函数的第二个参数应该指向的函数类型一致
alarm函数(闹钟函数):
unsigned int alarm(unsigned int seconds);
参数为注册的时间(单位为秒),时间到了产生SIGALRM信号,若传递0,则取消之前预约的SIGALRM信号,并返回剩余的时间(一个进程只能存在一个闹钟),如果想循环设置闹钟,应该在其对应的信号处理函数中再次调用alarm函数注册时间
sigaction函数(与signal函数相比,此函数更值得使用)
int sigaction(int signo, const struct sigaction *act, struct sigaction *oldact);
参数一信号名
参数二信号对应的信号处理函数
参数三 输出参数,传递结构地变量的地址以获取指向之前的信号处理函数的指针,不需要则传递NULL
struct sigaction
{
void (*sa_handler)(int);
sigset_t sa_mask;
int sa_flags;
}
结构体的成员sa_handler保存信号处理函数的地址,sa_mask和sa_flags初始化为0即可
示例:
struct sigaction act;
act.sa_handler = timeout;
sigemptyset(&act.sa_mask); //设置sa_mask成员的所有位为0
act.sa_flags = 0;
sigaction(SIGALRM, &act, 0);
既然子进程结束,操作系统会有一个SIGCHLD信号,那么久可以将wait()或waitpid()函数写入信号处理函数,然后用SIGCHLD信号与信号处理函数去向操作系统注册,这样,一旦子进程结束,操作系统会发出信号,并调用信号处理函数处理子进程
fork()函数与套接字
fork()函数不会不会将原来的套接字复制一份,仅仅是复制了指向套接字文件描述符,复制后两个文件描述符指向同一套接字,因为套接字是属于操作系统的,进程仅仅是拥有代表相应套接字的文件描述符,而且如果真的”复制了套接字”,那么会出现“同一端口对应多个套接字”的情况,这明显是错误的
fork()之后,有了两个指向同一套接字的文件描述符,但只close()其中一个文件描述符不会销毁套接字,也就是说需要两个套接字描述符都需要被close()才行,因此,为了避免因为忘记、疏忽或其他原因造成套接字无法销毁,在fork()之后,立即将与当前进程无关的套接字文件描述符close()
TCP的I/O分割
分割I/O的好处:
1、程序实现更加简单,父子进程仅需要考虑各自的情况,编写发送/接收数据的函数,减小在同一进程中实现数据收发逻辑思考量
2、提高频繁交换数据的性能,相比在同一进程中实现收发,分割后不受另一种操作(接收/发送)的影响,可以连续接收/发送,因此在同等时间范围会提高传输数据量
基于多进程的并发服务器
服务器端
//每当有客户端请求连接时,服务器端创建子进程提高服务
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <signal.h>
#define BUF_SIZE 1024
void error_handling(const char *message);
void read_childproc(int sig);
int main(int argc, const char * argv[])
{
int serv_sock = -1, clnt_sock = -1;
struct sockaddr_in serv_adr, clnt_adr;
pid_t pid;
struct sigaction act;
socklen_t adr_sz, optlen;
int str_len = -1, state = -1, option = -1;
char buf[BUF_SIZE] = {0};
if( 2 != argc )
{
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);
optlen = sizeof(option);
option = 1;
setsockopt(serv_sock, SOL_SOCKET, SO_REUSEADDR, &option, optlen);
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( -1 == bind(serv_sock, (struct sockaddr *) &serv_adr, sizeof(serv_adr)) )
error_handling("bind() error");
if( -1 == listen(serv_sock, 5) )
error_handling("listen() error");
while (1)
{
adr_sz = sizeof(clnt_adr);
clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_adr, &adr_sz);
if( -1 == clnt_sock )
continue;
else
puts("new client connected...");
pid = fork(); //创建子进程
if( -1 == pid )
{
close(clnt_sock); //创建子进程失败时应该销毁套接字
continue;
}
if( 0 == pid ) //子进程运行模块
{
//现在父子进程都有指向同一监听套接字的文件描述符了,但子进程并不需要//监听套接字,close()
close(serv_sock);
while(0 != (str_len = read(clnt_sock, buf, BUF_SIZE)))
write(clnt_sock, buf, str_len);
close(clnt_sock); //子进程的服务提供完毕,close()
puts("client disconnected...");
return 0;
}
else
close(clnt_sock); //由子进程提供服务,父进程不需要客户端套接字,close()
}
close(serv_sock); //即将结束父进程,close()
return 0;
}
void error_handling(const char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
void read_childproc(int sig)
{
pid_t pid;
int status;
pid = waitpid(-1, &status, WNOHANG); //销毁子进程
if(WIFEXITED(status))
{
printf("removed proc id: %d \n", pid);
printf("child send : %d \n", WEXITSTATUS(status));
}
}
客户端
//客服端I/O分割,父进程负责接收数据,子进程负责发送数据
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 1024
void error_handling(const char *message);
void read_routime(int sock, char *buf); //接收数据
void write_routine(int sock, char *buf); //发送数据
int main(int argc, const char * argv[])
{
int sock = -1;
pid_t pid;
char buf[BUF_SIZE];
struct sockaddr_in serv_adr;
if( 3 != argc )
{
printf("Usage: %s <IP> <port> \n", argv[0]);
exit(1);
}
sock = socket(PF_INET, SOCK_STREAM, 0);
if( -1 == sock )
error_handling("socket() error");
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( -1 == connect(sock, (struct sockaddr *) &serv_adr, sizeof(serv_adr)) )
error_handling("connect() error");
//分割I/O,父进程负责接收数据,子进程负责发送数据。
pid = fork();
if( -1 == pid )
printf("fork() error");
else if( 0 == pid )
write_routine(sock, buf);
else
read_routime(sock, buf);
close(sock);
return 0;
}
void error_handling(const char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
void read_routime(int sock, char *buf)
{
while (1)
{
int str_len = read(sock, buf, BUF_SIZE);
if( 0 == str_len )
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,配合子进//程的close()发送EOF,不然不能中断连接!只close一个描述符时,套接字健//在,无法发送EOF,服务器端将继续循环,客户端主进程将继续循环。。。
shutdown(sock, SHUT_WR);
}
write(sock, buf, strlen(buf));
}
}
运行结果: