根据这篇文章pid_t的定义
知道了
pid_t的真实定义:实际他就是 int 类型的。
下面代码是在上一篇错误函数封装服务器客户端通信
基础上,升级后的简易的多并发代码,就是说,前面只能开一个客户端和服务器通信,下面的代码可以同时开多个客户端和服务器通信。
注意看代码里面fork的用法。这个单词就是克隆的意思,可以百度查一查具体怎么用。
里面还多了一个清零函数。
最后注意在wrap.h中把头文件添加上就好。
#include"wrap.h"
#define SERV_PORT 9527 //bind函数中的端口号
char buf[BUFSIZ], clint_IP[1024];//用来保存读到的客户端的数组,BUFSIZ默认4096
//上面第二个数组用来接收客户端IP地址
//void sys_err(const char *str)
//{
// //perror是用来输出错误的,如果某些函数调用不正确的话,调用perror会先输出错误号,然后输出你在perror()参数中指定的内容
// perror(str);//包含在stdlib.h这个头文件中
// exit(1);
//}
int main(int argc, char *argv[])
{
int ifd =0,cfd=0,ret;//保存文件描述符,用于后期和服务器建立连接
pid_t pid;//注意这个的定义类型
socklen_t clit_addr_len;
//定义bind函数中用到的ip地址结构体
struct sockaddr_in serv_addr, clit_addr;
bzero(&serv_addr,sizeof(serv_addr));//把从第一个参数地址起,长度为第二个参数的字节清零
serv_addr.sin_family = AF_INET;//协议簇地址,和第三个参数的区别在于,这个是地址类型,第三个是有效实际地址
//TCP/IP是一个网络通讯协bai议群,它包含了很多通信协议。这些协du议制定了网zhi络设备、计算机连入网络以及数据是如何dao在它们之间进行传输的标准。
//TCP协议又叫传输控制协议,作用于传输层。IP协议又叫网络间互联协议,工作在网络层。它们都是TCP/IP协议簇中非常重要的协议。
serv_addr.sin_port = htons(SERV_PORT);//端口这里需要字节序的转换!需要来回传的都需要转换!
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);//取出系统中任意有效的IP地址
//sin_family和sin_addr.s_addr的地址有什么区别?
//socket目的:创建一个套接字
//第一个参数是个地址,IPv4的地址。用来产生套接字ip地址的协议
//第二个参数是个协议,数据传输协议
//第三个参数是:你所选用的数据传输协议的代表协议,比如流式数据传输协议(SOCK_STREAM)的代表协议是tcp
//成功:返回指向新创建的socket的文件描述符,失败:返回-1,
ifd = Socket(AF_INET, SOCK_STREAM, 0);
//bind作用:将socket和bind的第二个参数中的地址、端口号捆绑起来,以便监听
//第一个参数套接字描述符、 第二个参数是个结构体指针,里面包含服务器的IP地址+端口号、第三个参数是这个地址的长度
Bind(ifd,(struct sockaddr *)&serv_addr,sizeof(serv_addr));//和上面返回描述符不同,这个返回的是0
Listen(ifd, 128);//第二个参数是它所能监听的最大上限数
clit_addr_len=sizeof(clit_addr);
while(1)
{
//第二个参数是接收链接客户端地址信息,含IP地址和端口号,所以前面不需要专门定义
//返回一个套接字
cfd=Accept(ifd, (struct sockaddr*)&clit_addr ,&clit_addr_len);//注意第三个参数类型,相比第bind多一个指针
pid= fork();
if(pid<0)
{
perr_exit("fork error");
//exit(1);
}else if(pid==0)//子进程
{
Close(ifd);//子进程关闭监听
break;
}else //父进程
{
close(cfd);//父进程关闭通信
continue;
}
}
if(pid==0)
{
for(;;)
{
ret=Read(cfd,buf,sizeof(buf));//返回字节数
if(ret==0)
{
close(cfd);
exit(1);
}
Write(STDOUT_FILENO,buf,ret);标准输出到屏幕
for(int i=0; i<ret;i++)
buf[i]=toupper(buf[i]);
Write(cfd,buf,ret);//把buf的内容写回到客户端
}
}
return 0;
}
wrap.h的头文件改动:
#ifndef __WRAP_H_
#define __WRAP_H_ //防止重复定义
#include<stdio.h>//输入输出
#include<stdlib.h>//动态内存分配
#include<string.h>
#include<strings.h>//针对bzero这个清零头文件
//linux下有这个头文件,vc下没有
//它包含很多UNIX系统服务的函数原型,eg:read write函数等
#include<unistd.h>
#include<signal.h>//有关信号的函数
#include<errno.h>
#include<pthread.h>//包含有关多线程的函数
#include <sys/socket.h>//套接字函数、bind函数头文件
#include<ctype.h>//转小写转大写函数的头文件,在第二个地址结构的时候用
#include<arpa/inet.h>//bind的头文件
void perr_exit(const char *s);
int Socket(int family, int type, int protocol);
int Bind(int fd, const struct sockaddr *sa, socklen_t salen);
int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr);
//int Connect(int fd, const struct sockaddr *sa, socklen_t salen);
int Listen(int fd, int backlog);
ssize_t Read(int fd, void *ptr, size_t nbytes);
ssize_t Write(int fd, const void *ptr, size_t nbytes);
int Close(int fd);
#endif
上面的函数注意:每一次退出客户端都是crtl+c,这样退出之后会产生僵尸进程,所以需要想办法改进
僵尸进程就是父进程没死,子进程死掉,子进程残留出一点程序,等着父进程给自己收尸,如果不管的话,残留的越来越多,就会对系统造成影响。
下面涉及到的函数,都在这里的链接处找到系统的学习一下,再回来看:
黑马Linux系统编程视频
一、
*wait(int status)函数
参数为传出参数,返回的是子进程得状态。
该函数得作用:
通俗讲解:
1.等待它的儿子(子进程)运行完。
2.帮他儿子收尸。
3.知道它儿子死的原因。
一般放在父进程里面!
针对第三个功能:
一般借助宏函数判断怎么死的:
1.WIFEXITED(status)为非0-进程正常结束(大写得含义wait if exited)
ExITSTATUS(status)如上宏为真,使用此宏一获取进程退出状态(有状态11,17等)(exit status)
2.WIFSIGNALED(status)为非0一进程异常终止(wait if signaled)
WTERMSIG(status)如上宏为真,使用此宏一取得使进程终止的那个信号的编号。(wait term signaled)
*因为一次wait(int status)函数只能回收一个子进程,所以
延伸一个函数:
*pid_t waitpid(pid_t pid,int status,int options);
这个函数的目的是:暂时停止目前进程的执行,直到有信号来到或子进程结束。子进程的结束状态值会由参数 status 返回。如果不在意结束状态值,则参数 status 可以设成 NULL。而子进程的进程识别码也会一起返回。参数 pid 为欲等待的子进程识别码。
头文件:
#include<sys/types.h>
#include<sys/wait.h>
返回:
成功:返回清理掉的子进程ID;
失败:-1(无子进程)
三个参数的定义:
参1: pid > 0 指定进程id(子进程识别码)回收
pid = -1 回收任意子进程
pid = 0 回收本组任意子进程
pid < -1 回收该进程组的任意子进程
参2: status:
代表返回: 成功:pid 失败 -1
status:传出参数
1: 阻塞等待子进程
2: 回收子进程资源
3: 获取子进程结束状态:1)WIFEXITED()真
WEXITSTATUS()获取子进程退出状态
2)WIFSIGNALED() 真
WTERMSIG()获取导致子进程终止的信号的编码
参数3
options提供了一些额外的选项来控制waitpid,参数 option 可以为 0 或可以用"|"运算符把它们连接起来使用,比如:
ret=waitpid(-1,NULL,WNOHANG | WUNTRACED);
如果我们不想使用它们,也可以把options设为0,如:
ret=waitpid(-1,NULL,0);
WNOHANG情况:
若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若结束,则返回该子进程的ID。
WUNTRACED 情况
若子进程进入暂停状态,则马上返回,但子进程的结束状态不予以理会。WIFSTOPPED(status)宏确定返回值是否对应与一个暂停子进程。
二、
第二个sigaction函数
**int sigaction(int signum, const struct sigaction act,
struct sigaction oldact);
这个函数的目的:检查或修改与指定信号相关联的处理动作(可同时两种操作)
头文件:
#include <signal.h> #include<sys/types.h>
#include<sys/wait.h>
这几个头文件即可。
参数的意义:
signum参数指出要捕获的信号类型,act参数指定新的信号处理方式,oldact参数输出先前信号的处理方式(如果不为NULL的话)。
回收子进程僵尸进程的代码:
其实就是加了上面两个函数,只不过这两个函数不太好理解,需要看视频好好学习一下就可以。同时记得在头文件里面加上#include <signal.h>
#include"wrap.h"
#define SERV_PORT 9527 //bind函数中的端口号
char buf[BUFSIZ], clint_IP[1024];//用来保存读到的客户端的数组,BUFSIZ默认4096
//上面第二个数组用来接收客户端IP地址
//void sys_err(const char *str)
//{
// //perror是用来输出错误的,如果某些函数调用不正确的话,调用perror会先输出错误号,然后输出你在perror()参数中指定的内容
// perror(str);//包含在stdlib.h这个头文件中
// exit(1);
//}
void docatch(int signo)
{
//第一个参数是回收任意一个子进程
//第二个参数是进程结束状态返回,不用管
//第三个代表轮询返回,如果进程还没结束,返回0,结束了返回对应ID
while(waitpid(0,NULL,WNOHANG));
return;
}
int main(int argc, char *argv[])
{
int ifd =0,cfd=0,ret;//保存文件描述符,用于后期和服务器建立连接
pid_t pid;//注意这个的定义类型
socklen_t clit_addr_len;
//定义bind函数中用到的ip地址结构体
struct sockaddr_in serv_addr, clit_addr;
bzero(&serv_addr,sizeof(serv_addr));//把从第一个参数地址起,长度为第二个参数的字节清零
serv_addr.sin_family = AF_INET;//协议簇地址,和第三个参数的区别在于,这个是地址类型,第三个是有效实际地址
//TCP/IP是一个网络通讯协bai议群,它包含了很多通信协议。这些协du议制定了网zhi络设备、计算机连入网络以及数据是如何dao在它们之间进行传输的标准。
//TCP协议又叫传输控制协议,作用于传输层。IP协议又叫网络间互联协议,工作在网络层。它们都是TCP/IP协议簇中非常重要的协议。
serv_addr.sin_port = htons(SERV_PORT);//端口这里需要字节序的转换!需要来回传的都需要转换!
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);//取出系统中任意有效的IP地址
//sin_family和sin_addr.s_addr的地址有什么区别?
//socket目的:创建一个套接字
//第一个参数是个地址,IPv4的地址。用来产生套接字ip地址的协议
//第二个参数是个协议,数据传输协议
//第三个参数是:你所选用的数据传输协议的代表协议,比如流式数据传输协议(SOCK_STREAM)的代表协议是tcp
//成功:返回指向新创建的socket的文件描述符,失败:返回-1,
ifd = Socket(AF_INET, SOCK_STREAM, 0);
//bind作用:将socket和bind的第二个参数中的地址、端口号捆绑起来,以便监听
//第一个参数套接字描述符、 第二个参数是个结构体指针,里面包含服务器的IP地址+端口号、第三个参数是这个地址的长度
Bind(ifd,(struct sockaddr *)&serv_addr,sizeof(serv_addr));//和上面返回描述符不同,这个返回的是0
Listen(ifd, 128);//第二个参数是它所能监听的最大上限数
clit_addr_len=sizeof(clit_addr);
while(1)
{
//第二个参数是接收链接客户端地址信息,含IP地址和端口号,所以前面不需要专门定义
//返回一个套接字
cfd=Accept(ifd, (struct sockaddr*)&clit_addr ,&clit_addr_len);//注意第三个参数类型,相比第bind多一个指针
pid= fork();
if(pid<0)
{
perr_exit("fork error");
//exit(1);
}else if(pid==0)//子进程
{
Close(ifd);//子进程关闭监听
break;
}else //父进程
{
struct sigaction act;
act.sa_handler = docatch;
sigemptyset(&act.sa_mask);//清空这个信号集,全变为0
//sigaddset();
act.sa_flags =0;//默认为0,信号中断函数执行期间默认屏蔽该信号
//第一个参数是信号类型,这个看我发的视频链接的信号部分,信号就相当于中断
//SIGCHID信号就被放到这里面,一旦产生这个信号,就运行你自己指定的函数程序
//每次状态改变,子进程会发SIGCHID给父进程
//子进程终止(即子进程死亡)
//· 子进程停止(即子进程暂停)
//· 子进程恢复(即子进程从暂停中恢复执行)
int ret=sigaction(SIGCHLD,&act,NULL);
if(ret!=0){ perr_exit("sigaction error");}
close(cfd);//父进程关闭通信
}
}
if(pid==0)
{
for(;;)
{
ret=Read(cfd,buf,sizeof(buf));//返回字节数
if(ret==0)
{
close(cfd);
exit(1);
}
Write(STDOUT_FILENO,buf,ret);标准输出到屏幕
for(int i=0; i<ret;i++)
buf[i]=toupper(buf[i]);
Write(cfd,buf,ret);//把buf的内容写回到客户端
}
}
return 0;
}
下面函数先不用看。
三、
线程创建函数:
int pthread_create(pthread_t *tidp,const pthread_attr_t *attr,
void *(start_rtn)(void),void *arg);
头文件:#include <pthread.h>
仔细看,是四个参数!
参数
第一个参数为指向线程标识符的指针。
第二个参数用来设置线程属性。
第三个参数是线程运行函数的起始地址。
最后一个参数是运行函数的参数。
返回值
若线程创建成功,则返回0。若线程创建失败,则返回出错编号,并且*thread中的内容是未定义的。
四、
int pthread_join(pthread_t thread, void **retval);
视频讲:专用于回收子线程。
函数pthread_join目的:用来等待一个线程的结束,线程间同步的操作。
头文件 : #include <pthread.h> (和创建函数一样!)
参数 1:thread: 线程标识符,即线程ID,标识唯一线程。
传出参数,接收传出来的线程ID
参数2:retval: 用户定义的指针,用来存储被等待线程的返回值。
传出参数,回收线程结束的状态!
返回值 :
0代表成功。
失败则返回的则是错误号。
五、
int pthread_detach(pthread_t thread);
函数目的:从状态上实现线程分离,注意不是指该线程独自占用地址空间。
该函数分析
暂时还不知道具体怎么用,就知道跟产生僵尸进程之类的有关。
参数 :thread: 线程标识符 (和上面的第一个参数一样)
返回:
成功:0;
失败:错误号
六、
void *memset(void *s, int ch, size_t n);
函数目的:将s中当前位置后面的n个字节用 ch 替换并返回 s 。它是对较大的结构体或数组进行清零操作的一种最快方法。
头文件; #include<string.h>
原型:extern void bzero(void *s, int n);
用法:#include <string.h>
功能:置字节字符串s的前n个字节为零。
说明:bzero无返回值。