线程与进程
线程与进程的概念和区别
进程简单来说就是一个正在运行的程序。包括其运行代码和运行代码所用的资源,一个CPU可以存在多个进程但是同一时间只允许一个进程工作。但CPU切换速度很快,给我们感觉像是所有进程同时运行。
线程是操作系统最小度量单位。线程和进程最大的区别就是共不共享数据,同时线程是进程的一部分,也就是进程可以由多个线程构成。进程好比火车,线程好比车厢。不同火车之间的信息当然不共享,所以用起来比较麻烦,比如说打个电话。而线程好比同一列火车上的不同车厢,走过去打个招呼就能就交流。
使用多线程还是多进程?…Emmm其实我也不知道…曾经我想处理一个图片,但是如果只用一个程序跑的话太慢了,只用一个CPU核,所以我分别尝试了多线程和多进程…结果我的多进程是好用的…所有CPU核全部跑满看着很得劲(当然可能是因为操作系统是WIN的,WIN本身更偏向于多进程,而UNIX类的更多偏向多进程)
创建线程与进程
创建进程
创建进程使用的是fork函数,工作原理如下:
从上图可以看出,进程是完完整整的把父进程的所有都复制给了子进程,包括数据段空间,代码段空间和堆栈空间等。
fork函数会返回一个值,如果这个值是0的话,就是子进程,是其他的话就是父进程。其他值是子进程的进程号。
下面做个小测试:
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main()
{
pid_t pid;
pid = fork();
if (pid == -1)
{
printf("Error");
return 0;
}
else if (pid == 0)
{
printf("Child");
}
else
{
printf("Parent %d\n",pid);
}
return 0;
}
结果如下…Child其实emmmm是输出结果但是吧,我猜父进程结束,子进程结束。命令行只管父进程?当然不是了…是因为父进程先于子进程结束所以会导致这样的状况,所以emmm就有了等待进程结束的命令waitpid。
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
int main()
{
pid_t pid,pid_wait;
int status;
pid = fork();
if (pid == -1)
{
printf("Error");
return 0;
}
else if (pid == 0)
{
printf("Child\n");
printf("?");
}
else
{
printf("Parent %d\n",pid);
pid_wait = waitpid(pid,&status,0);
printf("Child process %d returned!\n",pid_wait);
}
printf("What happened?: %d",pid);
return 0;
}
这就是先进入父进程然后输出了Parent 57616父进程卡住,等待子进程然后子进程结束输出what happened(pid=0判断子进程)然后父进程的What happen。破案了
进程之间的通讯
进程之间的通讯是个很麻烦的事情,有两种方式,一种是管道,一种是共享内存。管道这种方式其实蛮简单的感觉像UART半双工通讯,两个进程之间只能单独写或单独读。
例子如下:
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main()
{
int fd[2];
pid_t pid;
char buf[64] = "I'm parent!\n";
char line[64];
if (pipe(fd)!=0)
{
fprintf(stderr,"Fauk to create pipe!\n");
return 0;
}
pid=fork();
if (pid<0)
{
fprintf(stderr,"Fail to create");
return 0;
}
else if (0==pid)
{
close(fd[0]);//shutdown read
write(fd[1],buf,strlen(buf));
close(fd[1]);//shutdown write
}
else
{
close(fd[1]);
read(fd[0],line,64);
printf("Date from parents:%s",line);
close(fd[0]);
}
return 0;
}
第二种就是共享内存,共享内存就是在内存中开辟一段空间供不同进程访问。
写共享内存:
shmget()函数用来创建共享内存,第一个参数是一个特殊标识,只要不重复就可,但一般由ftok()函数生成,第二个参数是大小字节数,第三个是内存操作方式
shmat()是获得一个共享内存ID对应的起始地址。第二个参数是指定共享内存地址,0是首地址,第三个参数一般写0,让代表需要让系统决定共享内存地址
shmdt()分离一块共享内存,估摸着就是释放掉。
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
int main()
{
int shmid; // 定义共享内存id
char *ptr;
char *shm_str = "string in a share memory";
shmid = shmget(0x90, 1024, SHM_W|SHM_R|IPC_CREAT|IPC_EXCL); // 创建共享内存
if (-1==shmid)
perror("create share memory");
ptr = (char*)shmat(shmid, 0, 0); // 通过共享内存id获得共享内存地址
if ((void*)-1==ptr)
perror("get share memory");
strcpy(ptr, shm_str); // 把字符串写入共享内存
shmdt(ptr);
return 0;
}
读共享内存:
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
int main()
{
int shmid; // 定义共享内存id
char *ptr;
char *shm_str = "string in a share memory";
shmid = shmget(0x90, 1024, SHM_W|SHM_R|IPC_CREAT|IPC_EXCL); // 创建共享内存
if (-1==shmid)
perror("create share memory");
ptr = (char*)shmat(shmid, 0, 0); // 通过共享内存id获得共享内存地址
if ((void*)-1==ptr)
perror("get share memory");
strcpy(ptr, shm_str); // 把字符串写入共享内存
shmdt(ptr);
return 0;
}
如果再次运行写程序就会报错
原因是当前共享地址Key用过,毕竟我们写的是0x90是固定的,使用ipcs可以看到
其中的1024是我们创建的,可以用ipcrm -m 4751372释放掉 (4751372是shmid)
创建线程
我们先看个例子然后从例子中学习:
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void* thread_func(void *arg) // 线程函数
{
int *val = arg;
printf("Hi, I'm a thread!\n");
if (NULL!=arg) // 如果参数不为空,打印参数内容
printf("argument set: %d\n", *val);
}
int main()
{
pthread_t tid; // 线程ID
int t_arg = 100; // 给线程传入的参数值
if (pthread_create(&tid, NULL, thread_func, &t_arg)) // 创建线程
perror("Fail to create thread");
sleep(1); // 睡眠1秒,等待线程执行
printf("Main thread!\n");
return 0;
}
可以看到pthread_create函数有4个参数,第一个是线程ID最后会回写的,第二个是用来设置线程属性的,没有就NULL,第三个就是函数指针,指定线程函数,第四个就是指定函数的传入参数。如果创建成功就会返回0,不成功返回错误号。
PS:如果直接gcc -o 编译的话会报错,因为pthread.h不是标准库中的函数,所以要加上参数 -lphread进行编译
取消线程,看例子就能理解:
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void* thread_func(void *arg) // 线程函数
{
int *val = arg;
printf("Hi, I'm a thread!\n");
if (NULL!=arg) {
// 如果参数不为空,打印参数内容
while(1)
printf("argument set: %d\n", *val);
}
}
int main()
{
pthread_t tid; // 线程ID
int t_arg = 100; // 给线程传入的参数值
if (pthread_create(&tid, NULL, thread_func, &t_arg)) // 创建线程
perror("Fail to create thread");
sleep(1); // 睡眠1秒,等待线程执行
printf("Main thread!\n");
pthread_cancel(tid); // 取消线程
return 0;
}
输出结果:
…最前面一个Hi,I’m thread!然后无数个argumen…然后结束
网络通讯
基础就是大学生计算机基础课程里应该有学过这个图:
我们主要先看TCP/IP协议也就是传输层和网络互联层的。
IP协议负责数据包的传输管理,实现两个基本功能:寻址和分段
寻址:就是IP协议根据数据报头中的地址传送数据报文。而IP协议根据目的地址选择报文在网络中的传输路径的过程叫做路由。(大家是不是知道…路由器为啥叫路由器了…)
分段:就是为了适应在不同网络中传输TCP/IP协议产生的分段机制…
TCP协议是传输层协议,TCP是一个面向连接可靠传输的协议,TCP协议层会对数据包进行排列并错误检测,如果缺少数据包就会重传丢失数据包。(感觉UDP就是TCP的不稳定不安全版本)
Socket通讯
之前废话一堆…其实Socket通讯我觉得最重要,毕竟…怎么实现才最重要么…
面向连接的Socket通信
这是面向连接的Socket通信框图
我们实现的时候就根据这个框图走。
总结如下:
服务器端工作流程图:
- 使用Socket函数创建socket
- 通过bind函数把创建的socket句柄绑定到指定TCP端口
- 调用listen函数使socket处于监听状态,并设置监听队列大小
- 当客户机发送连接请求后,调用accept()函数接收客户端请求,与客户端建立连接
- 与客户端发送或接收数据
- 通讯完成后,用close关闭socket函数
客户端工作流程
- 使用socket函数创建socket
- 调用connect函数向服务器socket发起连接
- 连接建立后,进行数据读写
- 传输完毕后,使用close关闭socket
依旧从程序看操作,演示本机和本机通讯的例子:
服务器:
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdio.h>
#include <errno.h>
#define EHCO_PORT 8080
#define MAX_CLIENT_NUM 10
int main()
{
int sock_fd;
struct sockaddr_in serv_addr;
int clientfd;
struct sockaddr_in clientAdd;
char buff[101];
socklen_t len;
int closing =0;
int n;
/* 创建socket */
sock_fd = socket(AF_INET, SOCK_STREAM, 0);
if(sock_fd==-1) {
perror("create socket error!");
return 0;
} else {
printf("Success to create socket %d\n", sock_fd);
}
/* 设置server地址结构 */
bzero(&serv_addr, sizeof(serv_addr)); // 初始化结构占用的内存
serv_addr.sin_family = AF_INET; // 设置地址传输层类型
serv_addr.sin_port = htons(EHCO_PORT); // 设置监听端口
serv_addr.sin_addr.s_addr = htons(INADDR_ANY); // 设置服务器地址
bzero(&(serv_addr.sin_zero), 8);
/* 把地址和套接字绑定 */
if(bind(sock_fd, (struct sockaddr*)&serv_addr, sizeof(serv_addr))!= 0) {
printf("bind address fail! %d\n", errno);
close(sock_fd);
return 0;
} else {
printf("Success to bind address!\n");
}
/* 设置套接字监听 */
if(listen(sock_fd ,MAX_CLIENT_NUM) != 0) {
perror("listen socket error!\n");
close(sock_fd);
return 0;
} else {
printf("Success to listen\n");
}
/* 创建新连接对应的套接字 */
len = sizeof(clientAdd);
clientfd = accept(sock_fd, (struct sockaddr*)&clientAdd, &len);
if (clientfd<=0) {
perror("accept() error!\n");
close(sock_fd);
return 0;
}
/* 接收用户发来的数据 */
while((n = recv(clientfd,buff, 100,0 )) > 0) {
buff[n] = '\0'; // 给字符串加入结束符
printf("number of receive bytes = %d data = %s\n", n, buff); // 打印字符串长度和内容
fflush(stdout);
send(clientfd, buff, n, 0); // 发送字符串内容给客户端
if(strncmp(buff, "quit", 4) == 0) // 判断是否是退出命令
break;
}
close(clientfd); // 关闭新建的连接
close(sock_fd); // 关闭服务端监听的socket
return 0;
}
客户端:
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdio.h>
#include <errno.h>
#define EHCO_PORT 8080
#define MAX_COMMAND 5
int main()
{
int sock_fd;
struct sockaddr_in serv_addr;
char *buff[MAX_COMMAND] = {
"abc", "def", "test", "hello", "quit"};
char tmp_buf[100];
socklen_t len;
int n, i;
/* 创建socket */
sock_fd = socket(AF_INET, SOCK_STREAM, 0);
if(sock_fd==-1) {
perror("create socket error!");
return 0;
} else {
printf("Success to create socket %d\n", sock_fd);
}
/* 设置server地址结构 */
bzero(&serv_addr, sizeof(serv_addr)); // 初始化结构占用的内存
serv_addr.sin_family = AF_INET; // 设置地址传输层类型
serv_addr.sin_port = htons(EHCO_PORT); // 设置监听端口
serv_addr.sin_addr.s_addr = htons(INADDR_ANY); // 设置服务器地址
bzero(&(serv_addr.sin_zero), 8);
/* 连接到服务端 */
if (-1==connect(sock_fd, (struct sockaddr*)&serv_addr, sizeof(serv_addr))) {
perror("connect() error!\n");
close(sock_fd);
return 0;
}
printf("Success connect to server!\n");
/* 发送并接收缓冲的数据 */
for (i=0;i<MAX_COMMAND;i++) {
send(sock_fd, buff[i], 100, 0); // 发送数据给服务端
n = recv(sock_fd, tmp_buf, 100, 0); // 从服务端接收数据
tmp_buf[n] = '\0'; // 给字符串添加结束标志
printf("data send: %s receive: %s\n", buff[i], tmp_buf); // 打印字符串
if (0==strncmp(tmp_buf, "quit", 4)) // 判断是否是退出命令
break;
}
close(sock_fd); // 关闭套接字
return 0;
}
其中AF_INET代表IPv4协议,地址的INADDR_ANY是本机地址也就是0.0.0.0
结果分析:运行服务器程序
会发现它阻塞在listen,等待客户端发送建立连接请求
然后运行客户端
接收发来的字符串,遇到quit就关闭连接。
无连接的Socket通讯
实现框图如下:
最大差别就是没有listen、accpet和connect这些连接环节。其次就是发送和接收函数的改变。
服务器代码:
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdio.h>
#include <errno.h>
#include <time.h>
#define TIME_PORT 9090
#define DATA_SIZE 256
int main()
{
int sock_fd;
struct sockaddr_in local;
struct sockaddr_in from;
int fromlen, n;
char buff[DATA_SIZE];
time_t cur_time;
sock_fd = socket(AF_INET, SOCK_DGRAM, 0); // 建立套接字
if (sock_fd<=0) {
perror("create socket error!");
return 0;
}
perror("Create socket");
/* 设置要绑定的IP和端口 */
local.sin_family=AF_INET;
local.sin_port=htons(TIME_PORT);// 监听端口
local.sin_addr.s_addr=INADDR_ANY;//本机
/* 绑定本机到套接字 */
if (0!=bind(sock_fd,(struct sockaddr*)&local,sizeof(local))) {
perror("bind socket error!");
close(sock_fd);
return 0;
}
printf("Bind socket");
fromlen =sizeof(from);
printf("waiting request from client...\n");
while (1)
{
n = recvfrom(sock_fd, buff, sizeof(buff), 0, (struct sockaddr*)&from, &fromlen); // 接收数据
if (n<=0) {
perror("recv data!\n");
close(sock_fd);
return 0;
}
buff[n]='\0'; // 设置字符串结束符
printf("client request: %s\n", buff); // 打印接收到的字符串
if (0==strncmp(buff, "quit", 4)) // 判断是否退出
break;
if (0==strncmp(buff, "time", 4)) {
// 判断是否请求时间
cur_time = time(NULL);
strcpy(buff, asctime(gmtime(&cur_time))); // 生成当前时间字符串
sendto(sock_fd, buff,sizeof(buff), 0,(struct sockaddr*)&from,fromlen); // 发送时间给客户端
}
}
close(sock_fd); // 关闭套接字
return 0;
}
运行服务器,卡在while等待数据
运行客户端
Socket超时处理
getsockopt()和setsockopt()
使用Select处理多连接
因为当recv()函数是阻塞的,导致等待一个客户端返回数据的时候造成整个进程阻塞,而无法接受其他客户端的数据。所以Socket库提供两个函数select()和poll()来解决这个问题。
从书上截图…等用的时候再说…这些标志太多了没有用的话太难懂了。