写在前面:注意,本章除了讲解进程管理,还包含网络编程Socket API的知识。

这里写目录标题

  • 一、进程
    • 1.1基础知识
      • 1.1.1进程ID
      • 1.1.2查看进程
      • 1.1.2 父子进程概念
      • 1.1.3得到进程ID的函数
    • 1.2 进程运行
      • 1.2.1 方法一
        • 1.2.1.1进程创建
        • 1.2.1.2 进程终止
      • 1.2.2 方法二
    • 1.2进程间通信(IPC)
      • 1.2.1信号
        • 1.2.1.1基础知识
        • 1.2.1.2 signal()、kill()、alarm()、sleep()
          • 1.2.1.2.1 signal()
          • 1.2.1.2.2 信号的发送方式
          • 1.2.1.2.3 kill()
          • 1.2.1.2.4 alarm()
          • 1.2.1.2.5 sleep()
        • 1.2.1.3 信号集和信号屏蔽
          • 1.2.1.3.1信号集
          • 1.2.1.3.2信号屏蔽技术
        • 1.2.1.4 sigaction()
        • 1.2.1.5 计时器产生信号
      • 1.2.2 管道
      • 1.2.3 XSI IPC
      • 1.2.3.1 共享内存
      • 1.2.3.2 消息队列
      • 1.2.3.3 信号量集
      • 1.2.3.3.1 信号量集应用_与共享内存技术结合
    • 1.2.4 Socket(包括IPC和网络通信两部分内容)
      • 1.2.4.1 网络常识
        • (1) OSI 与协议
        • (2) IP地址与端口
        • (3)字节顺序
      • 1.2.4.2 Socket通信
        • 1.2.4.2.1 Socket本地通信(IPC)
        • 1.2.4.2.2 Socket网络通信
          • 1.2.4.2.2.1 基于TCP的编程
          • 1.2.4.2.2.2 基于UDP的编程
  • 二、线程
    • 2.1基础知识
    • 2.2线程运行
    • 2.3 多线程通信
    • 2.3线程同步
      • 2.3.1 线程同步技术之互斥量技术
      • 2.3.2 线程同步技术之-信号量
    • 2.4 死锁
  • 三、一个综合案例

进程和程序的概念
程序是硬盘的文件,是代码的编译链接产物。
运行起来的程序就是进程,进程是内存中运行的程序。
操作系统支持多进程的启动,每个进程内部支持多线程。进程之间是完全独立的。

一、进程

1.1基础知识

1.1.1进程ID

进程用 进程ID 做唯一的标识,叫PID,函数getpid()可以获取当前进程的pid。

1.1.2查看进程

查看进程的方式:
1、Windows用ctrl + alt + delete启动任务管理器;
2、Linux/Unix用ps命令查看进程。

ps 只能显示当前终端启动的进程。
ps -aux : Linux专用选项,Unix不直接支持(/usr/usb/ps可以用)。
ps -ef : Unix/Linux通用的选项

如果进程比较多,可以用管道实现分页,命令如下:
ps -aux | more
实现空格翻页、回车翻行、q退出。

ps命令可以查看进程的如下信息:
进程PID、进程的启动者(属主)、CPU和内存使用率、状态、
父进程的PID、启动的程序是哪个
其中,进程的状态主要状态包括:
S - 休眠状态,进程大多数处于休眠状态
s - 说明该进程有子进程(父进程)
R - 正在运行的进程
Z - 僵尸进程(已经结束但资源没有回收的进程)

1.1.2 父子进程概念

关于父进程和子进程
操作系统中的多进程是有启动的次序的,Unix系统先启动0进程,0进程再启动进程1和进程2(有些系统只启动进程1),然后0进程就休眠。进程1和进程2启动其他进程,其他进程再启动其他的进程,直到所有的进程都启动为止。
如果进程a启动了进程b,a叫b的父进程,b叫a的子进程。

1.1.3得到进程ID的函数

进程用PID表示,PID是一个非负的正数,PID可以延迟重用。因此PID在同一
时刻保证唯一。

几个常用的函数:
getpid() - 取当前进程的PID
getppid() - 取当前进程的父进程的PID
getupid() - 取当前用户的ID。

1.2 进程运行

创建子进程的方法:
方法一: fork() 创建子进程,通过复制父进程创建子进程。因此父进程对应相同的代码区。
方法二:vfork() + execl()创建子进程,父进程和子进程的代码区完全不同,父子进程执行
的是完全不同的代码。

1.2.1 方法一

1.2.1.1进程创建

fork()是一个非常复杂的简单函数:

pid_t fork();

返回子进程的PID或者0,失败返回-1.没有参数。

fork()是通过复制父进程的内存空间创建子进程,复制除了代码区之外的所有区域,
代码区父子进程共享(只读)。
fork()创建一个子进程,子进程从fork()当前位置开始执行,fork()之前的代码父进程
执行一次,fork()之后的代码父子进程分别执行一次(共2次)
fork()函数自身会返回两次,父进程返回子进程的PID,子进程会返回0.注意了是函数的返回。
fork()创建子进程时,会复制除了代码区之外的所有区域,包括缓冲区。
fork()创建子进程时,如果父进程有文件描述符,子进程会复制文件描述符,不复制
文件表(父子进程共用一个文件表)。

父子进程的关系:
fork()创建子进程后,父子进程同时运行,如果子进程先结束,子进程会给父进程发信号,父进程回收子进程的资源。
fork()创建子进程后,父子进程同时运行,如果父进程先结束,子进程会变成孤儿进程,认进程1(init进程)做新的父进程。init进程也叫孤儿院。
fork()创建子进程后,父子进程同时运行,如果子结束时父进程没有收到信号或没有及时处理,子进程将变成僵尸进程。
具体fork()原理参考下图:

例子一:
fork1.c

#include <stdio.h>
#include <unistd.h>
int main() {printf("begin\n");pid_t pid = fork();printf("end%d\n",pid);
}
//执行结果:
begin
end11710
end0
如果begin没有加\n
#include <stdio.h>
#include <unistd.h>
int main() {printf("begin");//begin在输出缓冲区,子进程复制pid_t pid = fork();//缓冲区,而不是执行第5行printf("end%d\n",pid);
}
//执行结果:
beginend11710
beginend0

fork2.c

#include <stdio.h>
#include <unistd.h>
int main() {//父子进程使用不同的分支pid_t pid = fork();if(!pid){//父子进程都有,但子进程符合条件printf("我是子进程\n");}else{//父子进程都有,父进程执行,子进程不进行printf("我是父进程\n");}
}

例子二:要求在父子进程中,分别打印出父子进程的PID
//格式:我是子进程1234,我是父进程1233

#include <stdio.h>
#include <unistd.h>
int main() {pid_t pid = fork();if(!pid){//父子进程都有,但子进程符合条件printf("我是子进程%d,父进程是%d\n",getpid(),getppid());//getpid()获得当前pid,getppid()获得当前的父进程pid}else{//父子进程都有,父进程执行,子进程不进行printf("我是父进程%d,子进程是%d\n",getpid(),pid);}
}

fork3.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int i1 = 10;
int main() {int i2 = 10;int* pi = malloc(4);*pi = 10;pid_t pid = fork();if(!pid) {//子进程执行的分支i1 = 20; i2 = 20; *pi = 20;printf("child:i1=%d,i2=%d,*pi=%d\n",i1,i2,*pi);//打印结果是i1=20 i2=20 *pi=20printf("child:i1=%p,i2=%p,*pi=%p\n",&i1,&i2,pi);//fork()创建的子进程会复制父进程的虚拟内存地址,但映射到//不同的物理内存上,同时把原来的值拷贝过来。复制完成后父进//程的内存就独立了。exit(0);//结束子进程}sleep(1);//父进程 printf("father:i1=%d,i2=%d,*pi=%d\n",i1,i2,*pi);//打印结果是i1=10 i2=10 *pi=10//为什么是10,可以参考上面的图,改变子进程的栈区堆区全局//区,父进程的是不变的printf("father:i1=%p,i2=%p,*pi=%p\n",&i1,&i2,pi);//打印的虚拟内存地址和子进程一样
}

练习三:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
int main() {int fd = open("a.txt",O_RDWR|O_CREAT|O_TRUNC,0666);if(fd == -1)perror("open"),exit(-1);pid_t pid = fork();//有两个fd,子进程将复制fdif(pid == 0){//子进程执行的分支write(fd,"hello",5);//只复制描述符,不复制文件表close(fd);//关闭子进程的fdexit(0);}write(fd,"12345",5);close(fd);//关闭父进程的fdreturn 0;
}
//运行结果是12345hello(也就是说父和子进程没有相互覆盖,
因为不复制文件表,共用一个文件表,只有一个文件偏移量)

如果代码改成下面的,先fork(),在open"a.txt",则会出现hello和12345的覆盖

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
int main() {pid_t pid = fork();//先fork()的话,没有复制,而是创建int fd = open("a.txt",O_RDWR|O_CREAT|O_TRUNC,0666);if(fd == -1)perror("open"),exit(-1);if(pid == 0){//子进程执行的分支write(fd,"hello",5);//只复制描述符,不复制文件表close(fd);//关闭子进程的fdexit(0);}write(fd,"12345",5);close(fd);//关闭父进程的fdreturn 0;
}

fork()与open()关系如下图:

例子四:接下来这个程序验证父和子进程是否同时进行:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main() {pid_t pid = fork();if(!pid) {//子进程的分支printf("子进程是%d,父进程是%d\n",getpid(),getppid());//getpid()获得当前pidsleep(2);//休眠2秒printf("子进程是%d,父进程是%d\n",getpid(),getppid());exit(0);}sleep(1);//导致父进程先结束printf("pid = %d\n",getpid());//父进程pidreturn 0;
}
执行结果:
lioker:Desktop$ gcc 1.c
lioker:Desktop$ ./a.out
子进程是12760,父进程是12759
pid = 12759
lioker:Desktop$ 子进程是12760,父进程是1862
//因为父进程先结束,所以子进程重新认定一个父进程

1.2.1.2 进程终止

接下来讲进程的退出
正常退出:
1在main()中执行return语句
2执行exit()函数
3执行_Exit()或_exit()函数
4最后一个线程退出
5主线程退出
非正常退出:
1被信号打断导致退出
2最后一个线程被取消

今天研究exit()、_exit()和_Exit()都是用来退出进程的,区别在于:
_exit和_Exit()是一样的,区别在于头文件不同(uc/标c)
exit()和_Exit()区别主要在于退出的方式不同:
_Exit()是立即退出,exit()不是立即退出,还可以调用一些其他函数后再退出。
可以使用atexit()函数注册一些函数,这个函数在exit()之前会被自动调用,return也会调用。
补充一下函数指针:

void fa(void);//函数声明
void (*fa)(void);//函数指针(fa是函数名)

以下为exit()、atexit()函数程序代码:

#include <stdio.h>
#include <stdlib.h>
void fa(void){printf("fa called\n");
}
int main() {atexit(fa);//注册退出前的函数fa,现在不调用printf("begin\n");exit(0);printf("end\n");
}//运行结果为begin -> fa call -> end

改为下面的:

#include <stdio.h>
#include <stdlib.h>
void fa(void){printf("fa called\n");
}
int main() {atexit(fa);//注册退出前的函数fa,现在不调用printf("begin\n");_exit(0);printf("end\n");
}//运行结果为begin

再改为下面的:

#include <stdio.h>
#include <stdlib.h>
void fa(void){printf("fa called\n");
}
int main() {atexit(fa);//注册退出前的函数fa,现在不调用printf("begin\n");printf("end\n");
}//运行结果为begin -> end -> fa call。这是因为系统自动在
//主函数最后加了return 0;

wait()和waitpid()
wait和waitpid()可以让父进程等待子进程的结束,并取得子进程的退出状态和退出码(return后面的值或exit()括号中的值)
wait()和waitpid()的区别在于wait()很固定,而waitpid()更灵活。
wait()是等待任意一个子进程结束后返回,而waitpid()可以选择等待的子进程,也可以不等待。
wait()等待的子进程包括僵尸子进程,因此wait()也叫殓尸工。

pid_t wait(int* status)

参数是一个传出参数(指针),用来带出结束子进程的退出码和退出状态;
关于返回,有结束子进程就返回他的PID,没有就等待,父进程自己阻塞,如果出了错就返回-1。
宏函数WIFEXITED(status)可以判断是否正常退出,
WEXITSTATUS(status)可以取到退出码。
代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main() {pid_t pid = fork();if(!pid) {//子进程的分支sleep(2);printf("子进程%d即将结束\n",getpid());exit(100);//进程正常退出,并且完成了功能//exit(-1),负数,表示进程正常退出,但没有完成任务}int status;pid_t wpid = wait(&status);//如果子进程不结束,父进程则阻塞等待子进程结束printf("等待到了%d的退出\n",wpid);if(WIFEXITED(status)){//说明正常退出了printf("正常退出,退出码:%d\n",WEXITSTATUS(status));}
}
//运行结果:
lioker:Desktop$ gcc 1.c
lioker:Desktop$ ./a.out
子进程13436即将结束
等待到了13436的退出
正常退出,退出码:100

waitpid()有更多的选择

pid_t waitpid(pid_t pid,int *status,int option);

参数ststus和wait()一样,pid可以等待哪些或哪个子进程,option可以设定是否等待。
pid的值可能是:
== -1 等待任意子进程,与wait()等效; >0 等待指定子进程(指定pid);
==0 等待本进程组的任一子进程; <-1 等待进程组ID等于pid绝对值的任一子进程
注:后两个有时候用不到,了解即可
option的值:0 阻塞,父进程等待;WNOHANG 不阻塞,直接返回0
关于返回值:有子进程结束时返回子进程的pid,出错返回-1
如果为阻塞方式,没有子进程结束则继续等待;
如果是WNOHANG,若子进程不立即可用,则不阻塞,返回0。
代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main() {pid_t pid = fork();if(pid == -1)perror("fork"),exit(-1);if(!pid) {//子进程的分支sleep(1);printf("子进程%d即将结束\n",getpid());exit(100);}pid_t pie2 = fork(); if(!pid2){sleep(3);printf("子进程%d即将结束\n",getpid());exit(200);}int status;pid_t wpid = waitpid(pid2,&status,0);//如果waitpid()里的pid2改成-1即//waitpid(-1,&status,0)则pid和pid2//谁先结束选谁if(WIFEXITED(status)){printf("等到了%d子进程,退出码:%d\n",wpid,WEXITSTATUS(status));}
}

1.2.2 方法二

接下来讲vfork() + execl()方式创建子进程:

vfork()和fork()在语法上没有区别,唯一的区别在于vfork()不复制父进程的任何资源,而是直接占用父进程的资源运行代码,父进程处于阻塞状态,直到子进程结束或者调用了exec系列函数(比如:execl())。
vfork()和execl()的合作方式:
vfork()可以创建新的进程,但没有代码和数据,execl()创建不了进程,但可以为进程提供代码和数据。
和fork()不同,vfork()创建子进程后确保子进程先运行,父进程到子进程调用到了execl()之后才能运行。
注:vfork()如果占用的是父进程的资源,必须用exit()显式退出。

代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(){pid_t pid = vfork();if(!pid){printf("子进程%d开始运行\n",getpid());sleep(3);printf("子进程%d结束\n",getpid());exit(0);//vfork()占用父进程资源,必须用exit()退出}printf("父进程结束\n");return 0;
}

execl()是exec系统函数中的第一个,功能是启动一个全新的进程,替换当前的进程。启动的这个进程会全面覆盖旧进程,但不会新建进程。(会替换各种区域,但pid不变)

execl("程序的路径","执行的命令","参数",NULL);

只有第一个参数是必须正确的,第二个参数必须存在但可以不正确,第三个和第四个参数可以没有,NULL代表参数结束了。
比如运行我们的程序:

execl("./b.out","b.out",NULL);

代码如下:

#include <stdio.h>
#include <unistd.h>
int main() {printf("begin\n");execl("./bin/ls","ls","-l",NULL);printf("end\n");
}
//运行结果:
begin
total 16
-rw------- 1 lioker lioker  131 4月  21 17:45 1.c
-rwxrwxr-x 1 lioker lioker 8648 4月  21 17:45 a.out
这是因为execl()覆盖掉了旧进程,所以printf("end\n");未执行

下面把execl()与vfork()函数结合使用

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(){pid_t pid = vfork();if(!pid){execl("/bin/ls","ls","-l",NULL);printf("child\n");//打印不出来,不执行exit(0);//有意义,针对execl()出错时,退出子进程}printf("父进程开始运行\n");sleep(1);
}
//运行结果:
父进程开始运行
total 16
-rw------- 1 lioker lioker  212 4月  21 18:04 1.c
-rwxrwxr-x 1 lioker lioker 8800 4月  21 18:05 a.out

代码:两个.c文件
proc.c

#include <stdio.h>
int main(){printf("pid=%d\n",getpid());
}

vfork3.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(){pid_t pid = vfork();if(!pid){printf("pid=%d\n",getpid);execl("./proc","proc",NULL);exit(0);//有意义,针对execl()出错时,退出子进程}printf("父进程开始运行\n");sleep(1);
}
//执行结果:
pid=10583
父进程开始运行
pid=10583
也就是说,两个pid一样。这是因为execl()的pid不会改变

1.2进程间通信(IPC)

IPC - 进程间通信(InterProcess Communication, IPC)

Unix系统早期都是多进程解决问题的,因此多个进程之间需要交互数据,而进程间不能直接交互数据,IPC就是解决这个问题的。(后来就又出现了多线程技术)。

IPC主要包括:
1、文件
2、信号
3、管道
4、共享内存
5、消息队列
6、信号量集(与信号没有任何的关系)
7、网络socket

其中,共享内存、消息队列和信号量集都是XSI IPC,遵循相同的规范。因此编程有很多共性的地方。
而管道是古老的IPC,目前很少使用。
下面是IPC原理图:

1.2.1信号

1.2.1.1基础知识

信号是Unix/Linux系统下最常见的一种软件中断方式

中断就是让程序停止当前正在运行的代码,转而执行其他代码的过程。

中断分为软件中断和硬件中断,软件中断就是用软件的方式中断代码。

信号导致代码中断的案例很多,比如:
ctrl C ->信号2
kill -9 -> 信号9
段错误 -> 信号11
总线错误 -> 信号7(不确定,不同系统不一定)
Unix系统信号从1到48,Linux系统从1-64,但不确定连续,而且规范中没有规定信号的数量。

信号都有一个宏名称,以SIG开头,比如:信号2叫SIGINT,宏名称的本质就是一个非负整数,查看信号的命令:
kill -l
注:编程时,信号使用宏名称,因为有些系统信号数字不同,但宏名称是一样的。

常见的一些宏名称:
SIGINT - 信号2 ctrl+C
SIGQUIT - 信号3 ctrl+
SIGKILL - 信号9
… …

在Linux系统中,1-31是不可靠信号,是早期的信号,不支持排队,所以有可能丢失;34-64是可靠信号,支持排队,不可能丢失。信号分为可靠信号和不可靠信号。(可以比喻成,饭店生意太火,如果有地方支持排队吃饭,就是可靠。饭店太小不支持排队,顾客就走了丢失了)。
当信号是不可靠信号时进程只处理一个,当信号是可靠信号时,进程则依据排队一个一个处理。

信号的处理方式:信号只是一个整数,实现中断的功能依靠信号处理。信号的处理方式有三种:
1默认处理,是系统提供的,多半是退出进程。
2忽略信号,信号来了不做额外处理,直接忽略。
3自定义处理函数,信号按程序员的代码处理。
注:有些信号是不能被自定义和忽略了,比如信号9直接将进程强制退出;
进程可以给其他进程发信号,但只能给本用户的进程发信号,root可以给所有的用户进程发信号。

1.2.1.2 signal()、kill()、alarm()、sleep()

提前说明:kill()函数和kill命令的作用是向进程发送信号;而signal()函数的作用是接收信号并把信号传递给特定的函数中,就是说得有外界发送一个信号之后(比如按键盘ctrl+C)signal()才能收到信号然后起作用。

1.2.1.2.1 signal()

signal()/sigaction()可以设置信号的处理方式
signal()使用了一个函数指针,原型:
void (*f)(int)
函数指针 signal(int 信号值,函数指针)

其中,第二个参数函数指针可以是以下三个值:
SIG_IGN - 忽略该信号
SIG_DFL - 恢复默认处理
传入一个程序员自定义函数名 - 自定义处理函数

返回,成功返回之前的处理方式,失败返回SIG_ERR。
注:signal()函数只是设定了信号处理方式,自身并没有发信号,因此信号处理函数在信号到来时才执行。

kill命令用于发信号,格式:
kill -信号 进程pid
信号0没有实际的意义,用于测试是否有发信号的权限。
代码:

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
int main(){printf("pid=%d\n",getpid());while(1);
}
//在一个终端运行:
lioker:Desktop$ ./a.out
pid=15428
再打开一个终端:kill -0 15428则1无反应
kill -11 15428则段错误Segmentation fault (core dumped)
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
void fa(int signo){//系统会自动把信号的值传递给参数printf("捕获了信号%d\n",signo);
}
int main(){signal(SIGINT,fa);//信号2交给faif(signal(SIGQUIT,SIG_IGN) == SIG_ERR)//达到把信号3忽略的目的perror("signal 3"),exit(-1);signal(9,fa);//这里信号9调signal()没作用   printf("pid=%d\n",getpid());while(1);
}
//执行结果:
pid=15547
^C捕获了信号2
^C捕获了信号2
^\^\^\^\^\^\
Killed
就是说,在另一个终端执行kill -9 15547,这个进程才被杀死。

注意了,关于常用的信号的用法:
1、头文件<signal.h>
2、fa这个函数
3、signal(SIGINT,fa);signal(SIGQUIT,SIG_IGN) ;
改成下面的:

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
void fa(int signo){//系统会自动把信号的值传递给参数printf("捕获了信号%d\n",signo);signal(signo,SIG_DFL);//第一次给fa,第二次恢复默认
}
int main(){signal(SIGINT,fa);//信号2交给faif(signal(SIGQUIT,SIG_IGN) == SIG_ERR)//达到把信号3忽略的目的perror("signal 3"),exit(-1);signal(9,fa);//这里信号9调signal()没作用   printf("pid=%d\n",getpid());while(1);
}
//执行结果:
pid=15547
^C捕获了信号2
在输入一次^C就终止了,因为恢复默认了

父子进程之间的信号处理:
如果父进程用fork()创建的子进程,子进程的与父进程的信号处理方式完全一致(照抄)。如果父进程用vfork()+execl()方式创建的子进程,父进程自定义处理函数的子进程改为默认(因为子进程已经没有了处理函数),其他照抄。
代码验证一下:
fork.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
void fa(int signo){printf("捕获了信号%d\n",signo);
}
int main(){signal(SIGINT,fa);//信号2交给了fasignal(SIGQUIT,SIG_IGN);//忽略信号3pid_t pid = fork();if(!pid){printf("child:pid=%d\n",getpid());while(1);}printf("父进程%d结束\n",getpid());
}
lioker:Desktop$ gcc 1.c
lioker:Desktop$ ./a.out
父进程16192结束
child:pid=16193
lioker:Desktop$ kill -2 16193
捕获了信号2
lioker:Desktop$ kill -3 16193//信号3被忽略
lioker:Desktop$ kill -9 16193//被杀死

练习:验证一下vfork()+execl()的情况
proc.c

proc.c
#include <stdio.h>
int main(){while(1);
}vfork.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
void fa(int signo){printf("捕获了信号%d\n",signo);
}
int main(){signal(SIGINT,fa);//信号2交给了fasignal(SIGQUIT,SIG_IGN);//忽略信号3pid_t pid = vfork();if(!pid){printf("child:pid=%d\n",getpid());execl("./proc","proc",NULL);exit(0);}printf("父进程%d结束\n",getpid());
}
lioker:Desktop$ ./signal_02.out
child:pid=60908
父进程60907结束
lioker:Desktop$ kill -2 60901
bash: kill: (60901) - No such process
1.2.1.2.2 信号的发送方式

1、键盘发送(部分信号) Ctrl+C -> 信号2 Ctrl+\ -> 信号3
2、程序错误(部分信号) 段错误、总线错误、正数被0除
3、kill命令(所有信号) kill-信号 进程PID
4、系统函数(所有信号) raise()、kill()、alarm()、sigqueue…

我们要重点知道的是kill(),要了解的是alarm()

1.2.1.2.3 kill()
int kill(pid_t pid,int signo);

参数pid指定了给哪几个或哪些进程发信号,和waitpid()中pid一样。signo就是发送信号几。
返回:成功返回0,失败返回-1。

killall a.out可以杀死所有的叫a.out的进程

pid的值:
正数:就是给指定的一个进程发信号(pid)
-1 :就是给所有进程发信号(有权限的)
0 :本组所有的进程发信号
<-1 :指定进程组(组ID=-pid)的所有进程
最常用的是正数,其次是-1。

kill.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
void fa(int signo){printf("捕获了信号%d\n",signo);
}
int main(){signal(SIGINT,fa);pid_t pid = fork();if(!pid){printf("pid=%d\n",getpid());while(1);}kill(pid,SIGINT);//产生SIGINT信号printf("父进程结束\n");
}
执行结果:
//lioker:Desktop$ ./a.out
父进程结束
捕获了信号2//子进程打出的。
pid=16844
1.2.1.2.4 alarm()
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void fa(int signo){//printf("捕获了信号%d\n",signo);printf("扫描张飞菜园子,开始偷菜\n");}
int main() {printf("pid=%d\n",getpid());signal(SIGALRM,fa);alarm(3);//3秒以后产生一个SIGALRM信号//alarm()可以做计划任务//alarm(0)就是取消闹钟,返回之前的剩余秒数while(1);
}
1.2.1.2.5 sleep()

接下来讲sleep()和usleep()函数

sleep()函数会让程序休眠,直到休眠到时间到,或者到未忽略的信号的到来。会返回剩余的秒数。
usleep()函数更精确,休眠是以微秒为单位。

#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void fa(int signo){printf("捕获了信号%d\n",signo);
}int main(){signal(SIGINT,fa);signal(SIGQUIT,SIG_IGN);int res = sleep(10);//返回剩余的秒数printf("game over,left sec:%d\n",res);return 0;
}

1.2.1.3 信号集和信号屏蔽

1.2.1.3.1信号集

系统定义了存放多个信号的数据结构,就是信号集sigset_t,其本质是一个超大的整数,每个二进制位代表一个信号,比如:信号2用倒数第二位代表,倒数第二位是1代表有信号2,是0代表没有。比如8位信号集:1011 0111

信号集的运算结构(提供函数):
增加元素(信号)
删除元素(信号)
查询元素(信号)
sigaddset()-增加一个信号(某个二进制置一)
sigfillset()-所有信号全部增加(所有位全置一)
sigdelset()-删除一个信号
sigemptyset()-删除所有信号(所有位都置0)
sigismember()-查询一个信号是否在

写代码:

#include <stdio.h>
#include <signal.h>
int main(){sigset_t set;printf("size=%d\n",sizeof(set));printf("%d\n",set);sigemptyset(&set);//清空printf("%d\n",set);//0sigaddset(&set,2);//倒数第二位置1sigaddset(&set,3);//倒数第三位置1printf("%d\n",set);//2+4=6sigaddset(&set,7);printf("%d\n",set);//64+4+2=70sigdelset(&set,3);//70-4=66if(sigismember(&set,2) == 1)printf("信号2在\n");
}
1.2.1.3.2信号屏蔽技术

信号什么时候来是无法确定和控制的。但可以使用信号屏蔽技术让信号的处理时间延后。信号屏蔽主要用于关键代码的执行,不是阻止信号的到来,而是将信号的处理延后。关键代码执行完毕后一定要解除信号的屏蔽,信号得到处理。

举个例子:
银行转账 ->1先转出账户,把金额减少
(这时候信号来了,但是可以暂不处理)
2再转入账户,把金额增加
(转完之后,信号在这里处理)
解除信号的屏蔽
函数sigprocmask()可以屏蔽/解除屏蔽。

int sigprocmask(int how,sigset_t* new,sigset_t* old)

参数how是信号屏蔽方式;
new是新的权限屏蔽字(屏蔽哪些权限);
old(是传出参数,用于传出以前的信号屏蔽字,便于后面恢复)。

how的三种方式:
SIG_BLOCK:之前的屏蔽+new的屏蔽
A B C + C D E -> A B C D E
SIG_UNBLOCK:之前的屏蔽-new的屏蔽
A B C - C D E -> A B
SIG_SETMASK:直接用新的屏蔽,与之前的无关
A B D C D E -> C D E
SIG_SETMASK最常用,记住这个就行。

以下这个代码是说,在屏蔽时,按Ctrl+C不起作用

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
void fa(int signo){printf("捕获了信号%d\n",signo);
}
int main() {signal(SIGINT,fa);//2是不排队信号,不排队,就是说多次按Ctrl+c却只执行一次signal(45,fa);//45是可靠信号,排队,来几次处理几次。//注:在另一个终端输入kill -45 进程PIDprintf("%d执行普通代码,信号不屏蔽\n",getpid());sleep(15);printf("执行关键代码开始信号屏蔽\n");sigset_t set,old;sigemptyset(&set);//清空sigaddset(&set,2);sigaddset(&set,45);sigprocmask(SIG_SETMASK,&set,&old);//屏蔽信号2和45,并把//原来的屏蔽状态给oldsleep(10);printf("关键代码执行完毕,开始解除屏蔽\n");sigprocmask(SIG_SETMASK,&old,NULL);sleep(20);printf("程序结束\n");
}

1.2.1.4 sigaction()

sigaction()的绝大多数的功能都由signal()实现,一些特殊的功能sigaction()可以实现。sigaction()可以让信号有更多的信息,甚至可以在发信号的时候附带额外的数据。sigaction()了解就行。

1.2.1.5 计时器产生信号

计时器(了解)

Linux计时器就是N秒之后,每隔M秒产生一个信号。
计时器有三种:真实、虚拟和实用,常用的是真实计时器,真实计时器产生SIGALRM信号。
setitimer()可以设置计时器,getitimer()可以获取计时器。

以下这个代码是一个例子,这个例子等你用到的时候再看就行,不用记。

#include <stdio.h>
#include <sys/time.h>
#include <signal.h>
void fa(int signo){printf("I am superman");
}
int main(){signal(SIGALRM,fa);struct itimerval it;it.it_interval.tv_sec = 1;//结构体里面还有一个结构体//间隔时间整数秒为1秒it.it_interval.tv_usec = 10000;//间隔时间的微秒数为0.1秒//所以总共间隔时间为1.1秒it.it_value.tv_sec = 3;//开始时间的秒数it.it_value.tv_usec = 0;//开始计时的秒数总共为3秒整setitimer(ITIMER_REAL,&it,0);//设置计时器为真实计时器,把传进来//第三个参数设置为0//自动生成一个SIGALRM信号                     while(1);
}

1.2.2 管道

管道(了解)

管道是以文件做交互的媒介,这种文件比较古老,是管道文件。

管道分为有名管道(有文件名)和无名管道(没有名字)。有名管道就是程序员自己创建管道文件,然后进行交互。无名管道就是系统帮我们创建管道文件,利用系统的管道文件进行交互。

有名管道适用于所有的进程的通信,无名管道只能用于fork()创建的父子进程之间的通信。

管道文件无法用touch/vi/open()函数创建,创建管道文件需要使用mkfifo命令或者mkfifo(),管道文件创建完毕后,再用读写文件的方式进行通信。

管道文件不存储数据,只是倒转一下数据。比如收张三的二手车让他9点送过来,把二手车卖给李四,让李四11点过来提车。有点像这个机制

一个例子,在一个终端里输入:

lioker:Desktop$ mkfifo a.pipe
lioker:Desktop$ cat a.pipe

在另一个终端里输入:

lioker:Desktop$ ls > a.pipe

这时第一个终端就是打印出ls并变成:

lioker:Desktop$ mkfifo a.pipe
lioker:Desktop$ cat a.pipe
1.c
a.out
a.pipe

做个练习:
写pipea.c和pipeb.c,pipea.c读a.pipe,pipeb.c写a.pipe,读写的数据是100个整数。注意,链接成两个不同的可执行文件,并测试。
pipeb.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
int main(){int fd = open("a.pipe",O_WRONLY);//不能用O_RDWR,//因为这样一来读写管道都有了,//直接结束了if(fd == -1)perror("fopen"),exit(-1);for(int i=0;i<100;i++){write(fd,&i,4);}close(fd);
}

pipea.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
int main(){int fd = open("a.pipe",O_RDONLY);if(fd == -1)perror("fopen"),exit(-1);int x;while(1){int res = read(fd,&x,sizeof(x));if(res == -1){perror("read");break;}if(!res)break;printf("x=%d\n",x);}close(fd);
}

1.2.3 XSI IPC

XSI IPC消息队列、信号量集、共享内存统称为XSI IPC
XSI IPC的共性(固定步骤):

1、创建/获取 IPC结构(共享内存、消息队列和信号量集)必须先提供一个外部的key。
2、每个IPC结构都有一个唯一的ID与之对应,可以用key拿到id.
3、外部key的类型是 key_t,获得key的方式有三种:
a:使用宏IPC_PRIVATE做key,这个key基本不用,因为key只能创建,不能获取。(这个方法有但不使用)
b:可以用函数ftok()创建key
c:可以在一个公共的头文件中定义每个使用的key,key本身就是一个整数。
4、可以用函数xxxget()创建/获得id,比如: shmget()/msgget()
5、每种IPC结构都提供了一个xxxctl()函数,可以修改、删除、查询IPC结构。
6、使用key新建IPC结构时,参数flg一般都是IPC_CREAT|权限。
7、xxxctl()函数中,cmd支持以下宏:
IPC_STAT:用于查询: IPC_SET: 用于修改 IPC_RMID:用于删除

1.2.3.1 共享内存

以上都是三者共性的东西,下面是关于共享内存的:
内核管理一片物理内存,允许不同的进程映射,因此多个进程可以映射同一片物理内存。共享内存是最快的IPC。映射物理内存叫挂接,用完以后接触映射叫脱接
共享内存的编程步骤:
1、先获得key,可以使用头文件或者ftok()
2、用key获得/创建一个共享内存的ID,函数shmget()。
3、映射共享内存,挂接,函数shmat()。
4、数据交互。IPC
5、解除映射,脱接,函数shmdt()。
6、如果确定共享内存内部不再使用,可以使用shmctl()函数删除。

IPC结构可以使用命令进行查看或者删除:
ipcs ------- 可以查询IPC
ipcrm ---- —可以删除IPC
ipcs - a 查看所有IPC
- m 查看共享内存
- q 查看消息队列
- s 查看消息量级
ipcrm 删除时,需要提供IPC的ID。

ftok()通过一个真实存在的路径+项目ID(0-255)生成一个key

关于ftok()的感悟:key_t key = ftok(".",100);//100是项目的ID,随便给的;.是当前路径
1、ftok()是干嘛的?
答:利用ftok()产生一个key。比如进程a产生一个key,进程b也产生一个和a进程数值一样的key,那么进程b就可以和
a进行通信了。
2、怎么使用ftok()?
答:第一个参数是一个真实路径(你的进程文件所在的路径,你要保证你的几个进程的真实路径是一样的),比如a.c
在./,你的b.c也在./,想让给他们俩进行通讯就设置路径为. ;
第二个参数是一个自己随便设置的值(即项目ID),但是你要保证你要通讯的几个进程的项目ID一样。
也就是说在同一个路径下的,且项目ID相同的两个进程,所形成的key的数值是一样的,因此这俩进程可以通信。
3、fyok()形成key的原理
答:一个独特的路径,和一个独特的项目ID,有且仅形成一个key数值。系统中key相同的进程可以进行通讯
int shmget(key_t key,size_t size,int flag);

shmget()函数的作用是申请一块共享内存。
size_t size是指申请的共享内存的大小。是字节数。
flag新建共享内存时给取IPC_CREAT|权限,当获取共享内存时则取0。
返回共享内存的ID,失败返回-1

void *shmat(int shmid, const void *shmaddr, int shmflg)

shmat()作用是挂接,把进程的虚拟内存与共享内存挂接上。
但是挂接到进程虚拟内存的哪个地方呢?答案是可以用shmaddr来设置,当然可以直接设置为0让系统来决定挂接在哪个地方。
shmflg的设置:SHM_RDONLY为只读模式,而其他为读写模式。
shmat()返回进程挂接的地址。

shma.c

#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/shm.h>
int main(){key_t key = ftok(".",100);//100是项目的ID,随便给的if(key == -1)perror("ftok"),exit(-1);int shmid = shmget(key,4,IPC_CREAT|0666);if(shmid == -1)perror("shmget"),exit(-1);void *p = shmat(shmid,0,0);int *pi = p;*pi = 10;shmdt(p);
}
//运行这个代码,可以用ipcs -m查看共享内存,发现共享内存
的数量较原来的多一个

//写一个练习,写shmb.c,从共享内存中取出数据10
shmb.c

#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/shm.h>
int main(){key_t key = ftok(".",100);if(key == -1)perror("ftok"),exit(-1);int shmid = shmget(key,0,0);if(shmid == -1)perror("shmget"),exit(-1);int *p = shmat(shmid,0,0);printf("*pi = %p",*p);shmdt(p);
}

下面讲一下shmctl()函数

#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/shm.h>
int main() {key_t key = ftok(".",100);int shmid = shmget(key,4,IPC_CREAT|0666);if(shimd == -1)perror("shmget"),exit(-1);struct shmid_ds ds;shmctl(shmid,IPC_STAT,&ds);//查询printf("shmid=%d\n",shmid);printf("size=%d\n",ds.shm_segsz);//打印查询结果中的大小printf("nattch=%d\n",ds.shm_nattch);//挂接数printf("mode=%o\n",ds.shm_perm.mode);//权限//下面修改ds.shm_segsz = 400;ds.shm_perm.mode = 0644;shmctl(shmid,IPC_SET,&ds);//修改//但是会发现ds.shm_segsz = 400;改不了//ds.shm_perm.mode = 0644;能改//就是说只能修改权限shmctl(shmid,IPC_RMID,0);//删除
}

共享内存的缺点是多个进程同时写的时候,数据完全混乱了。而消息队列可以解决这个问题。
当然了,其实共享内存配合信号量集也可以解决这个同步(进程互斥)问题。

1.2.3.2 消息队列

消息队列可以解决这个问题:
消息队列设计更加的合理,先把数据封入消息中,把消息存入队列中,从队列中取出或放入数据。
消息队列是学习的重点,其也是采用内存做交互媒介,系统的内核管理一
个队列,队列中存放着数据。

消息队列的使用步骤:
1、用ftok()获得外部的key。
2、用msgget()创建/获取消息内存的ID。
3、放入数据(msgsnd())或者取出数据(msgrcv())。
4、如果确定不再使用消息队列,用msgctl()删除消息队列。

msga.c

#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int main() {key_t key = ftok(".",100);int msgid = msgget(key,IPC_CREAT|0666);if(msgid == -1)perror("msgget"),exit(-1);int res = msgsnd(msgid,"hello",5,0);//第四个参数为0表示队列满的时候等待//IPC_NOWAIT为满的时候不等待if(!res) printf("发送成功\n");return 0;
}

下面写msgb.c程序,取出msga.c放入的程序
msgb.c

#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int main() {key_t key = ftok(".",100);int msgid = msgget(key,0);if(msgid == -1)perror("msgget"),exit(-1);char buf[100] = {};int res = msgrcv(msgid,buf,sizeof(buf),0,0);//第四个参数为0表示队列满的时候等待//IPC_NOWAIT为满的时候不等待if(!res) printf("接收成功%d字节%d\n",res,buf);return 0;
}

上面的程序不是规范的消息队列的用法。

消息队列的正规用法:
消息分为有类型消息和无类型消息,无类型消息编程比较简单,但接收数据时无法细分,只能先入先出。有类型消息编程相对比较麻烦,接收数据时可以区分,但是不是先入先出。

消息是一个结构,格式如下:

struct 结构名{//结构名,程序员可以自定义long mtype;//消息类型,注意第一个成员必须是消息类型//数据区,支持任意类型的数据
};

把结构放入队列,或从队列中取出。

int msgsnd(int msgid,void* addr,size_t size,int flag);
参数:
msgid就是消息队列的ID,用key可以获得。
addr是消息的首地址,也就是消息队列的首地址。
size是消息中数据区的大小,不算类型。(算类型也可以)
flag可以为0代表阻塞,IPC_NOWAIT非阻塞(满了直接返回-1)
ssize_t msgrcv(int msgid,void* addr,size_t size,int msgtype,int flag)
参数:
smggid和addr这两个参数与msgsnd一样,size是接收buffer的大小,
flag和msgsnd一样
msgtype决定了接收何种类型的消息(消息类型必须大于0)
> 0 接收特定类型的消息
= 0    接收任意类型的消息
< 0  接收类型小于等于msgtype绝对值的消息,从小到大成功返回实际收到的字节数,失败返回-1。

下面是一个原理图:

对这个原理图进行编程:
(即改msgrcv(msgid,&msg,sizeof(msg.buf),2,0);倒数第二个参数)
msgtypea.c(传入数据)

#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>struct Msg{long mtype;char buf[256];
};
int main() {key_t key = ftok(".",100);int msgid = msgget(key,IPC_CREAT|0666);if(msgid == -1)perror("msgget"),exit(-1);struct Msg msg1,msg2;msg1.mtype = 1;strcpy(msg1.buf,"zhangfei");msg2.mtype = 2;strcpy(msg2.buf,"guanyu");msgsnd(msgid,&msg1,sizeof(msg1.buf),0);msgsnd(msgid,&msg2,sizeof(msg2.buf),0);
}

msgtypeb.c(传出数据)

#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>struct Msg{long mtype;char buf[256];
};
int main() {key_t key = ftok(".",100);int msgid = msgget(key,0);if(msgid == -1)perror("msgget"),exit(-1);struct Msg msg;int res = msgrcv(msgid,&msg,sizeof(msg.buf),2,0);//倒数第二个参数2代表取关羽if(res == -1)perror("msgrcv"),exit(-1);printf("%s\n",msg.buf);
}

函数msgctl(msgid,IPC_RMID,0)可以删除消息队列

注:消息队列的删除和共享内存的删除机制不同,共享内存的删除只是做了个标记,不能确保马上删除,只有挂接数为0的才能被删除(好比超市晚上9点关门,而8点半就只能只进不出)。
而消息队列随时可以删除,即使队列中还有消息依然会被删除,这就会有隐患。

一个消息队列技术的综合案例:
模拟ATM - 初级版
ATM包括:开户、销户、存钱、取钱、查询、转账六大功能,今天做开户功能,其他功能完成开户以后再做。
开户功能的实现流程:
1、写一些共性的内容,比如:
写一个账户的结构,包括:账户ID,户名姓名,密码,金额。
写一个消息结构,包括:mtype和账户。
消息类型定义成宏,提高可读性。包括:
开户、销户、存钱、取钱、查询、转账、处理成功、处理失败
2、写两个程序,服务端和客户端。
具体流程:

1、服务端先启动,先创建两个消息队列,服务端发给客户端的和客户端发给服务端的,等待客户端发消息(接收客户端发给服务端的消息队列中的消息)。
2、然后客户端启动,先打印提供一个界面,输入数字,完成功能,功能包括:开户、销户、存钱、取钱、查询、转账、退出。用户选择开户选项后,输入姓名、密码和金额,账户ID是由服务器自动生成。把数据封入消息,放入客户端发给服务器的消息队列,并等待服务端回发给客户端的消息(接收消息队列)。
3、服务端收到消息,进行消息处理。先分配一个不重复的账户账户ID,把ID和用户提交的数据放在一起,把数据存入文件中,如果存入成功,开户就ok;如果存入失败,开户就失败。把开户的结果封入消息中,放入服务端发给客户端的消息队列中。
4、客户端显示处理的结果给用户,打印开户成功,卡号;或开户失败
5、服务器支持关闭功能,用信号实现关闭,在关闭服务器的同时需要删除消息队列(在信号处理函数中删除)

思考:
1如何实现账户ID不重复?
先创建一个文件,存入一个初始值。每次开户时,先读出这个整数,然后整数++做
账户的ID,同时自增以后的值存回文件。
2账户文件如何设计?是所有账户一个文件还是每个账户一个文件?
编码每个账户一个文件更简单,以账户ID做文件名确保不重复。

msg.h文件:

#include <string.h>
#include <fcntl.h>
#include <unistd.h>struct mtext{char name[20];char password[20];float amount;
};
struct MsgInf{long mtype;struct mtext buf;
};struct MsgId{long mtype;int userId;
};

sever.c

#include "msg.h"int main(){struct MsgId MsgIdSed;MsgIdSed.mtype = 1;struct MsgInf MsgInfRcv;/*创建userid_seq.txt*/if(access("userid_seq.txt",F_OK)==-1){int fd1=open("userid_seq.txt",O_CREAT|O_WRONLY,0666);if(fd1 == -1){perror("fd1open");exit(-1);}int id = 1000;int resw = write(fd1,&id,sizeof(int));close(fd1);}/*接收客户端信息*/key_t key = ftok(".",100);int msgid = msgget(key,IPC_CREAT);if(msgid == -1)perror("msgget"),exit(-1);int ifrcv = 1;printf("开始准备接收数据\n");do{ifrcv = msgrcv(msgid,&MsgInfRcv,sizeof(MsgInfRcv.buf),1,IPC_NOWAIT);//倒数第二个参数代表取哪个sleep(2);printf("%d\n",ifrcv);if(ifrcv == -1)perror("msgrcv"),exit(-1);}while(ifrcv);sleep(5);printf("接收数据结束%d\n",ifrcv);/*取userid_seq.txt的id并存储新的id进去*/int fd1=open("userid_seq.txt",O_RDWR,0666);if(fd1==-1){perror("fd1open");exit(-1);}int id = 0;read(fd1,&id,sizeof(int));MsgIdSed.userId = id;id++;lseek(fd1,0,SEEK_SET);write(fd1,&id,sizeof(int));close(fd1);/*创建存储每个id信息的文件*/int norealid = MsgIdSed.userId;char filename[10];char *suffix = "dat";sprintf(filename,"%d.%s",norealid,suffix);  int fd = open(filename,O_CREAT|O_WRONLY|O_APPEND,0666);if(fd == -1)perror("open"),exit(-1);int res1 = write(fd,&MsgInfRcv.buf.name,sizeof(MsgInfRcv.buf.name));if(res1 == -1)perror("writename"),exit(-1);int res2 = write(fd,&MsgInfRcv.buf.password,sizeof(MsgInfRcv.buf.password));if(res2 == -1)perror("writepassword"),exit(-1);int res3 = write(fd,&MsgInfRcv.buf.amount,sizeof(MsgInfRcv.buf.amount));if(res3 == -1)perror("writeamount"),exit(-1);int res4 = write(fd,&MsgIdSed.userId,sizeof(MsgIdSed.userId));if(res4 == -1)perror("writeuserId"),exit(-1);int res5 = msgsnd(msgid,&MsgIdSed,sizeof(MsgIdSed.userId),0);if(res5 == -1)perror("msgsndMsgId"),exit(-1);return 0;
}

client.c

#include "msg.h"int SelectWork(){int num;printf("请选择业务\n\r开户请输入:1\n\r销户请输入:2\n\r存钱请输入:3\n\r取钱请输入:4\n\r");scanf("%d",&num);return num;
}int main(){struct MsgId MsgIdRev;key_t key = ftok(".",100);int msgid = msgget(key,IPC_CREAT|0666);if(msgid == -1)perror("msgget"),exit(-1);struct MsgInf MsgInfSed;MsgInfSed.mtype = 1;int num = SelectWork();/*选择业务*/if(num == 1){char buf[100];float amount;printf("请输入英文用户名\n");scanf("%s",buf);strcpy(MsgInfSed.buf.name,buf);printf("请输入密码\n");scanf("%s",buf);strcpy(MsgInfSed.buf.password,buf);printf("请输入金额\n");scanf("%f",&amount);MsgInfSed.buf.amount = amount;int res = msgsnd(msgid,&MsgInfSed,sizeof(MsgInfSed.buf),0);if(!res) printf("发送成功\n");}int ifrcv = 1;do{ifrcv = msgrcv(msgid,&MsgIdRev,sizeof(MsgIdRev.userId),1,IPC_NOWAIT);//如果接收到则ifrcv = 0}while(ifrcv);printf("打印成功!\n您的账号为%d\n",MsgIdRev.userId);return 0;
}

1.2.3.3 信号量集

信号量和信号没有任何的关系。
信号量集技术不是用于进程间传递数据、而是用于进程间访问控制。比如用于共享内存的进程互斥问题,以避免其多个进程同时访问同一内存导致冲突。

信号量是一个计数器,负责控制访问共享资源的最大并行进程总数。

信号量其实就是一个计数,初始值设为最大值,每进入一个进程则计数-1,每离开一个则计数+1,当计数到0阻塞进程,直到计数重新大于0解除阻塞。

如果有多个共享资源需要控制最大并行进程数,需要多个信号量,把多个信号量存入数组中,这个数组就是信号量集

IPC拿到的是信号量集,而不是单一的信号量。

信号量集的编程步骤:

1、获得key
2、使用函数semget()新建/获取信号量集的ID
3、使用函数semctl()为每个信号量设定初值的最大值。
4、正常使用,函数semop()可以对计数加1或减1
5、如果不再使用,semctl()删除信号量集

int  semctl(int semid,int semsemnum,int cmd,/*union senum arg*/)

参数semid就是信号量集(数组);
semnum就是信号量下标;
cmd决定了功能,新增功能SETVAL,可以设定单个信号量的初始最大值;
第四个参数是可选的联合,对于SETVAL来说,就是一个整数。

int semop(int semid,struct sembuf semoparray[],size_t nops);

1、参数semoparray是一个信号量操作数组
信号量操作sembuf 结构表示:
struct sembuf{
unsigned short sem_num;//操作信号量的下标
short sem_op;//-1代表计数减一,1代表计数加一
short sem_flg;//0代表阻塞,IPC_NOWAIT非阻塞,不等待
};
2、 nops是指semoparray元素的个数。

创建代码
sem1.c

#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int main() {key_t key = ftok("./",100);int semid = semget(key,1,IPC_CREAT|0666);/*1元素。就是说,你设置为了只有一个共享资源。你可以理解是一个数组,只有一个共享资源那么数组就是一个元素了*/if(semid == -1)perror("semid"),exit(-1);int res = semctl(semid,0,SETVAL,10);/*最多支持10个并行进程。注意,第二个参数为0的含义是,想让这10个进程的共享资源数组的0元素,即第一个共享资源(再说了你这里的semget()所设置的共享资源数组元素数也就一个)*/if(!res)printf("信号量集成功创建\n");
}

使用代码
sem2.c
这个代码要说明的是,来了20个子进程,只能有10个进程可以工作,剩下十个不可以工作。可以工作的10个进程每释放一个,则一个不可以工作的转入工作

#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <unistd.h>
int main() {key_t key = ftok("./",100);int semid = semget(key,0,0);//获取,不新建。第二个参数为第1个共享资源if(semid == -1)perror("semid"),exit(-1);int res = semctl(semid,0,SETVAL,10);/*最多支持10个并行进程。注意,第二个参数为0的含义是,想让这10个进程的共享资源数组的0元素,即第一个共享资源(再说了你这里的semget()所设置的共享资源数组元素数也就一个)*/if(!res)printf("信号量集成功创建\n");int i;for(i=0;i<20;i++){//20个子进程pid_t pid = fork();if(!pid){printf("申请一个计数\n");struct sembuf buf;buf.sem_num = 0;//下标,即表示第1个共享资源buf.sem_op = -1;//计数-1buf.sem_flg = 0;//可以是IPC_NOWAIT semop(semid,&buf,1);//操作buf//数组就是首元素的地址printf("申请成功\n");sleep(10);buf.sem_op = 1;//计数+1printf("释放一个计数\n");semop(semid,&buf,1);//操作bufexit(0);}}
}
在终端上显示:
申请一个计数
申请成功
.
.(共10个,证明申请成功了)
.
申请一个计数申请成功
申请一个计数
.
.(共10个,证明申请未成功)
.
申请成功释放一个计数
申请成功
.
.(共10个,证明有的进程释放后,之前未申请成功的,
.               这时候即可申请成功)
释放一个计数
申请成功

上述的创建代码sem1.c代码有需要优化的地方,如下:
sem3.c

#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <signal.h>int semid;//多个函数都需要使用的变量,做成全局变量
void fa(int signo){printf("删除信号量集\n");semctl(semid,0,IPC_RMID);
}
int main() {printf(用Ctrl+C退出\n);signal(SIGINT,fa);//修改处理方式key_t key = ftok("./",100);int semid = semget(key,1,IPC_CREAT|0666);//1元素if(semid == -1)perror("semid"),exit(-1);int res = semctl(semid,0,SETVAL,10);//最多支持10个并行进程if(!res)printf("信号量集成功创建\n");while(1);//让进程一直保持存在,能接收信号
}
关于信号量集的一点感悟:
1、信号量集哪里体现出了进程间通信:
答:控制某共享资源的最大并行进程数,即在你的编程条件下有的进程可以工作,有的就不可以,这就是对各进程的
控制,即进程间通讯。并且信号量集的编程框架和消息队列、共享内存一样,因此都叫XSI IPC。

1.2.3.3.1 信号量集应用_与共享内存技术结合

为了理解更清晰,举例把共享内存技术和信号量集技术结合起来,使得共享内存的进程互斥。

进程1创建共享内存, 因为要和进程2访问同一个资源所以再创建一个信号量集,进程1的代码如下:(进程的互斥)

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/sem.h>
#include <signal.h>void *p = NULL;
int shmid,semid;void func(int sig)
{shmdt(p);shmctl(shmid,IPC_RMID,0);exit(0);
}int main()
{char buf[100] = {};signal(SIGINT,func);//1.获取keykey_t key = ftok(".",'x');if(key==-1){perror("ftok");exit(-1);}//2.创建共享内存shmid = shmget(key,100,IPC_CREAT|0666);if(shmid==-1){perror("shmget");exit(-1);}//3.映射共享内存p = shmat(shmid,0,0);if(p==(void *)-1){perror("shmat");exit(-1);}//4.创建信号量集semid = semget(key,1,IPC_CREAT|0666);if(semid==-1){perror("semget");exit(-1);}//5.设置信号量初始值1int res = semctl(semid,0,SETVAL,1);if(res==-1){perror("semctl");exit(-1);}while(1){//P操作, 信号量-1struct sembuf sem;sem.sem_num = 0;sem.sem_op = -1;sem.sem_flg = 0;semop(semid,&sem,1);//写共享内存,发送printf("请输入:");fgets(buf,sizeof(buf),stdin);buf[strlen(buf)-1] = 0;strcpy((char *)p,buf);//while(1);//V操作,信号量+1sem.sem_op = 1;semop(semid,&sem,1);}return 0;
}

进程2的代码如下:(进程的互斥)

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/sem.h>
#include <signal.h>void *p = NULL;
int shmid,semid;void func(int sig)
{shmdt(p);semctl(semid,0,IPC_RMID);exit(0);
}int main()
{signal(SIGINT,func);//1.获取keykey_t key = ftok(".",'x');if(key==-1){perror("ftok");exit(-1);}//2.共享内存shmid = shmget(key,0,0);if(shmid==-1){perror("shmget");exit(-1);}//3.映射共享内存p = shmat(shmid,0,0);if(p==(void *)-1){perror("shmat");exit(-1);}//4.信号量集semid = semget(key,0,0);if(semid==-1){perror("semget");exit(-1);}while(1){//P操作, 信号量-1struct sembuf sem;sem.sem_num = 0;sem.sem_op = -1;sem.sem_flg = 0;semop(semid,&sem,1);printf("%s\n",(char *)p);//V操作,信号量+1sem.sem_op = 1;semop(semid,&sem,1);}return 0;
}

运行结果:

到这里你可能还有一个疑惑:没有发现共享内存的资源和信号量级管理的资源联系起来啊?
答:实际上是联系起来了的,是在逻辑上的联系上而不是通过某个函数。你看,如果把进程1中的while()中的while(1)死循环解除掉注释,那么进程2一定无法读取共享内存,因为此时信号量集的下标为0的信号量的最大进程数为1个,进程1的while(1)不结束那么进程2就没有办法访问共享内存。

1.2.4 Socket(包括IPC和网络通信两部分内容)

1.2.4.1 网络常识

(1) OSI 与协议

七层模型,亦称OSI(Open System Interconnection)
ISO/OSI的七层模型:
物理层、数据链路层、网络层、传输层、会话层、表现层、应用层。
关于这7层模型,去看下面这个超链接网址:
https://blog.csdn.net/Zenian_dada/article/details/89340125

TCP/IP协议中将七层模型简化成了4层或5层:
5层的说法是会话层、表现层、应用层三合一,和成了一个大的应用层
4层的说法是再将物理层和数据链路层合并。

计算机中,数据交互的规矩制定成了协议。常见的协议
FTP - 文件传输协议
HTP - 超文本传输协议(互联网的基础协议)
TCP - 传输控制协议,是传输层的协议
UDP - 用户数据报协议,是传输层协议
pop3、smtp - email的发送和接收协议

单个协议功能有限,会把多个协议组合一个协议簇(族),协议簇以主体命名。

(2) IP地址与端口

IP地址和端口是软件工程师必须会的。
IP地址,就是计算机在网络中的唯一标识,网络中计算机用IP地址代表。
IP地址的本质就是个整数,绑定网卡的物理地址(Mac地址)。Mac地址在网卡出厂时都确保唯一。

IP地址有IPv4和IPv6之分,目前IPv4还是主流,是32位整数的代表IP地址,IPv6目前还不是主流,是128位整数,但后续的系统都是两个都支持。

IP地址应该是用整数代表,但这个整数很难直接记忆,为了便于记忆,把IP地址的每个字节看成一个数(8位整数0-255),即把地址分成4个数,中间用.隔开,这种方式叫点分十进制。如192.123.321.124。人类使用的是点分十进制,计算机底层存储的是整数格式(转为8位的十六进制)。这两种表示方法编程时需要相互转换。

但是即使这样,IP地址在记忆上还是很难,因此采用助记符代表IP,这个助记符就是域名(网址),域名需要转换成IP才能找到计算机。这个工作由域名解析服务器完成。

IP地址分为A、B、C、D四类IP。
IP地址只能定位到计算机,但不能定位到计算机的进程(如QQ与浏览器)。端口用于计算机对外管理进程。因此网络编程需要提供I地址+端口号这两者。

socket即IP地址+端口,通过socket把网络间的两个进程连接起来。

IP地址点分十进制和十六进制的转换:
192.168.0.23 -> C0 A8 00 17 -> 0xC0A80017(可以用计算器)

子网掩码(了解) - 用于判断两个IP地址是否同一网段
166.111.160.1
166.111.161.45
子网掩码:和255.255.254.0位与
示例:
166.111.160.1
255.255.254.0(位与)
-------------------
166.111.160.0166.111.161.45
255.255.254.0(位与)
-------------------
166.111.160.0
得知结果一样,故属于同一网段

端口本质是一个short(0-65536),代表了计算机中的一个进程。这些端口中,有些端口已经被占用,要使用不被占用的端口。
0-1023系统预先占用了这里面的大多数,最好别用。
1024 - 4万8千,但中间有个端口被软件占用。
4万8千多 - 65535系统随时可能会用一个(非稳定的)

常见端口:
80 - HTTP端口
21 - FTP端口
23 - telnet端口

(3)字节顺序

字节顺序 - 计算机存储数据的方式有所不同,比如int类型。有些计算机存储是从高位到低位字节,而有些计算机时从低位到高位字节。

1 2 3 4 -> 1 2 3 4
4 3 2 1

网络中如果传输整数,就会产生倒了的问题。本机的格式时无法统一的,但网络格式是统一的,因此:编程时可能需要把本机格式转换成网络格式,或把网络格式还原成本机格式。

1.2.4.2 Socket通信

Unix系统在网络上功能非常强大,历史悠久,因此有一个非常固定的套路,代码比较僵化,没有改变的余地。

socket编程主要包括两个方面:
本地通信 - 计算机内部的进程间的通信(IPC)
网络编程 - 网络编程(重点)

1.2.4.2.1 Socket本地通信(IPC)

Socket的本地通信 - 本机程序之间的通讯(IPC)

socket编程步骤:
网络编程要考虑两个方面:服务器端和客户端
1、服务器端的编程步骤

1.1调用函数socket(),用来创建一个socket描述符。
1.2准备通信地址(三个结构体),进行数据交互
1.3绑定通信地址和socket描述符,函数bind()。
1.4读写描述符,和读写文件描述符方式一样,函数read()/write()
1.5函数close();关闭socket描述符。

2、客户端的编程步骤:

除了1.3的bind()换成connect()之外,其他的与服务器一样。
而且connect()和bind()用法完全一样。

函数的解释:

int socket(int domain,int type,int protocol)

一、参数 domain用于选择选择协议簇,备选用宏: AF_UNIX/AF_LOCAL/AF_FILE:本地通信
AF_INET4:网络通信IPv4(主要用)
AF_INET6:网络通信IPv6
注:AF换成PF效果一样。

二、 参数type选择通信类型,主要包括:
SOCK_STREAM:数据流 用于TCP
SOCK_DGRAM :数据报,用于UDP

三、第三个参数protocol本来应该指定协议,但基本不用,因为协议已经被前两个参数确定了,所以给0即可。

四、函数返回:成功返回socket描述符,失败返回-1.

准备通信地址需要三个结构体,包括:

struct sockaddr、struct sockaddr_un、struct sockaddr_in

其中,sockaddr主要用于做函数的参数,不负责存储数据;
sockaddr_un负责存本地通信的地址数据;
sockaddr_in负责存网络通信的地址数据。

#include <sys/un.h>
struct sockaddr_un{int sun_family;//用于指定协议簇,和socket()一致char sun_path[];//存socket文件名(做交互媒介)
};
#include <netinet/in.h>
struct sockaddr_in{int sin_family;//用于指定协议簇,和socket()一致short sin_port;端口号struct in_addr sin_addr;//存储IP地址的结构
};

注:sockaddr_in/sockaddr_un做参数时必须类型转换为sockaddr

int bind(int sockfd,struct sockaddr* addr,socklen_t size)

参数sockfd就是socket描述符,socket()函数的返回值
addr是通信地址的指针,需要做类型转换。
size是通信地址的大小,sizeof(struct)

注意:读写函数时,一方读数据,另一方必须写数据。

先写本地版代码socka.c:

#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <string.h>int main() {int fd = socket(AF_UNIX,SOCK_DGRAM,0);//1-返回socket描述符if(fd == -1)perror("socket"),exit(-1);struct sockaddr_un addr;//2- 通信地址addr.sun_family = AF_UNIX;strcpy(addr.sun_path,"a.sock");//系统自动建文件int res= bind(fd,//3 - 开放一个端口(struct sockaddr*)&addr,sizeof(addr));if(res == -1)perror("bind"),exit(-1);printf("bind ok\n");char buf[100] = {};//读数据res = read(fd,buf,sizeof(buf));printf("读到了%d字节,内容%s\n",res,buf);close(fd);5 - 关闭
}

sockb.c:

#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <string.h>int main() {int fd = socket(AF_UNIX,SOCK_DGRAM,0);//1-返回socket描述符if(fd == -1)perror("socket"),exit(-1);struct sockaddr_un addr;//2- 通信地址addr.sun_family = AF_UNIX;strcpy(addr.sun_path,"a.sock");//系统自动建文件int res= connect(fd,//3 - 连接服务器(struct sockaddr*)&addr,sizeof(addr));if(res == -1)perror("connect"),exit(-1);   printf("connect ok\n");write(fd,"hello",5);//写数据close(fd);5 - 关闭
}
1.2.4.2.2 Socket网络通信

基于TCP/UDP的一对多网络编程(一个服务器,多个客户端)

注:强烈建议去看看《计算机网络》的P307 ~ P310页的讲解。

TCP协议和UDP协议(传输层)
TCP协议是一个基于连接的协议(有连接协议),在网络交互过程中,服务器和客户端要保持完整,不能断开。优点会重发一切错误数据,保证数据的正确性和完整性,缺点是资源消耗的比较大。
UDP协议是一个无连接协议,在网络交互过程中,不保持连接,只需要在发送数据时连一下。优点是资源的消耗小,缺点是数据可能不完整或不确。

一个知识点:
关于获得IP地址的一些命令:
ifconfig(Unix/Linux)
ipconfig(Windous DOS)
/sbin/ifconfig
判断网络是否畅通,用ping命令
ping IP地址 就知道网络是否畅通

1.2.4.2.2.1 基于TCP的编程

服务器的步骤:

1、调用socket()得到socket描述符
2、准备通信地址struct sockaddr_in
3、绑定bind()
4、监听,函数listen()
5、等待客户端的连接,函数accept(),返回一个新的socket描述符,这个新的socket描述符用于读写交互。
6、读写函数read()/write()
7、关闭socket()

客户端的步骤:

与前面讲的一样的。

listen()函数主要用于设置:当有多个客户端同时请求时,放入等待队列中,等待队列的最大长度就是listen()设置。
accept()函数:

int accept(int fd,struct socketaddr* addr,socklen_t* len)

参数fd就是第一步的返回值 addr是一个结构体指针,用于接收客户端的通信地址。
len是一个传入传出参数,出入addr的真实长度,传出接收到的客户端的通信地址的真实长度。(一般来是传入传出值相同)
返回:成功返回新的socket描述符,用于read()/write();失败返回-1。

服务器代码:

1.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>int main(){int fd = socket(AF_INET,SOCK_STREAM,0);//必须用stream,即用TCPif(fd == -1)perror("socket"),exit(-1);struct sockaddr_in addr;addr.sin_family = AF_INET;addr.sin_port = htons(2222);addr.sin_addr.s_addr = inet("192.168.124.10");int res = bind(fd,(struct sockaddr*)&addr,sizeof(addr));if(res == -1)perror("bind"),exit(-1);printf("bind ok");listen(fd,100);//等待队列1最大程度为100struct sockaddr_in form;socklen_t len = sizeof(form);//带值传入int sockfd = accept(fd,(struct sockaddr*)&from,&len);//用于阻塞的函数printf("%s连接了\n",inet_ntoa(from.sin_addr));char buf[100] = {};res = read(sockfd,buf,sizeof(buf));printf("读到了%d字节,内容:%s\n",res,buf);close(sockfd);close(fd);
}

客户端代码:

2.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>int main() {int fd = socket(AF_INET,SOCK_STREAM,0);//1-返回socket描述符if(fd == -1)perror("socket"),exit(-1);struct sockaddr_in addr;addr.sin_family = AF_INET;addr.sin_port = htons(2222);addr.sin_addr.s_addr = inet("192.168.124.10");int res = connect(fd,(struct sockaddr*)&addr,sizeof(addr));if(res == -1)perror("bind"),exit(-1);printf("connect ok");write(fd,"hello",5);//写数据close(fd);5 - 关闭
}

对1.c进行改进

#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>void fa(int signo){printf("服务器即将关闭\n");sleep(1);close(fd);exit(0);
}int main(){printf("按Ctrl+C退出\n");signal(SIGINT,fa);int fd = socket(AF_INET,SOCK_STREAM,0);//必须用stream,即用TCPif(fd == -1)perror("socket"),exit(-1);struct sockaddr_in addr;addr.sin_family = AF_INET;addr.sin_port = htons(2222);addr.sin_addr.s_addr = inet("192.168.124.10");//这两行是解决地址被占用问题int reuse = 1;setsockopt(fd,SOL_SOCKET,SO_REUSEADDR,&reuse,sizeof(reuse));int res = bind(fd,(struct sockaddr*)&addr,sizeof(addr));if(res == -1)perror("bind"),exit(-1);printf("bind ok");listen(fd,100);//等待队列1最大程度为100while(1){struct sockaddr_in form;socklen_t len = sizeof(form);//带值传入int sockfd = accept(fd,(struct sockaddr*)&from,&len);//用于阻塞的函数printf("%s连接了\n",inet_ntoa(from.sin_addr));char buf[100] = {};res = read(sockfd,buf,sizeof(buf));printf("读到了%d字节,内容:%s\n",res,buf);close(sockfd);}
}

服务器的作用主要是数据的处理和转发。

练习作业,增强刚才的代码:在客户端加上输入代码(scanf()),输入bye退出。服务器端要转发这些数据给客户端自己,服务器也需要加上循环。代码如下:

客户端:

#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>int main() {int fd = socket(AF_INET,SOCK_STREAM,0);//1-返回socket描述符if(fd == -1)perror("socket"),exit(-1);struct sockaddr_in addr;addr.sin_family = AF_INET;addr.sin_port = htons(2222);addr.sin_addr.s_addr = inet("192.168.124.10");int res = connect(fd,(struct sockaddr*)&addr,sizeof(addr));if(res == -1)perror("bind"),exit(-1);printf("connect ok");char buf1[100] = {};char buf2[100] = {};while(1){printf("请输入聊天内容\n");scanf("%s",buf1);write(fd,&buf,strlen(buf1));//写数据if(!strcmp(buf1,"bye"))break;int res = read(fd,buf2,sizeof(buf2));if(res == -1){perror("read");break;}printf("read:%s\n",buf2);memset(buf1,0,strlen(buf1));//清空buf1memset(buf2,0,strlen(buf2));//清空buf2}close(fd);5 - 关闭
}

服务器:

#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#include <string.h>void fa(int signo){printf("服务器即将关闭\n");sleep(1);close(fd);exit(0);
}int main(){printf("按Ctrl+C退出\n");signal(SIGINT,fa);int fd = socket(AF_INET,SOCK_STREAM,0);//必须用stream,即用TCPif(fd == -1)perror("socket"),exit(-1);struct sockaddr_in addr;addr.sin_family = AF_INET;addr.sin_port = htons(2222);addr.sin_addr.s_addr = inet("192.168.124.10");//这两行是解决地址被占用问题int reuse = 1;setsockopt(fd,SOL_SOCKET,SO_REUSEADDR,&reuse,sizeof(reuse));int res = bind(fd,(struct sockaddr*)&addr,sizeof(addr));if(res == -1)perror("bind"),exit(-1);printf("bind ok");listen(fd,100);//等待队列1最大程度为100while(1){struct sockaddr_in form;socklen_t len = sizeof(form);//带值传入int sockfd = accept(fd,(struct sockaddr*)&from,&len);//用于阻塞的函数printf("%s连接了\n",inet_ntoa(from.sin_addr));pid_t pid = fork();//accept()每当成功后都会创建一个子进程,以达到多人聊天的效果if(!pid){char buf[100] = {};while(1){res = read(sockfd,buf,sizeof(buf));printf("读到了%d字节,内容:%s\n",res,buf);if(res == -1){perror("read");break;}else if(!res)break;if(!strcmp(buf,"bye"))break;write(sockfd,buf,strlen(buf));memset(buf,0,strlen(buf));//清空buf}close(sockfd);exit(0);}close(sockfd);}
}
1.2.4.2.2.2 基于UDP的编程

接下来讲UDP协议-用户数据报协议(SOCK_DGRAM)

数据流可以理解为水流一直流;

数据报可以理解为牛奶一包一包的。

UDP编程步骤:(先接收方即首先发数据的一方)

1socket(),返回socket描述符
2准备通信地址,struct sockaddr_in
3绑定bind()
4读写
5关闭描述符
注:先发送方:编程步骤和上面的基本一样,省掉bind()即可

UDP使用的读写函数与TCP不同,在不连接(conect)的前提下:
读数据使用read()或recvfrom();
写数据适用sendto();
其中read()和recvfrom()的区别在于:
read()只能读数据,不能读发送方的通信地址,而recvfrom()两者皆可。

int sendto(int fd,void* addr,size_t len,int flag,struct sockaddr* addr,socklen_t addrlen)
参数:前三个和write()一样,flag给0即可。
addr传入数据接收方的通信地址,addrlen就是通信地址的长度。
返回:成功返回发送的字节数,失败返回-1。
int recvfrom(int fd,void* data,size_len,int flag,struct sockaddr* addr,socklen_t addrlen)
参数:前三个和read()一样,flag给0即可。
addr是一个传出参数,用于接收发送方的通信地址
addrlen是一个传入传出参数,出入addr的真实长度,传出接收到的客户端的通信地址的真实长度。(一般来是传入传出值相同)
返回:成功返回发送的字节数,失败返回-1。

服务器:

#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/n.h>
#include <arpa/inet.h>int main() {int fd = socket(AF_INET,SOCK_DGRAM,0);//1-返回socket描述符if(fd == -1)perror("socket"),exit(-1);struct sockaddr_in addr;//2- 通信地址addr.sin_family = AF_INET;addr.sin_port = htons(2222);//服务器的端口// htons(2222)是为了本机端口整数格式转为网络格式addr.sin_addr.s_addr = inet_addr("192.168.124.10");//服务器的地址//inet_addr("192.168.124.10")是转16进制int res= bind(fd,//3 - 开放一个端口(struct sockaddr*)&addr,sizeof(addr));if(res == -1)perror("bind"),exit(-1);printf("bind ok\n");char buf[100] = {};//4读数据//res = read(fd,buf,sizeof(buf));struct sockaddr_in from;scoklen_t len = sizeof(from);res = recvfrom(fd,buf,sizeof(buf),0,(struct sockaddr*)&from,&len);//接收地址printf("读到了%d字节,内容%s\n",res,buf);sendto(fd,"welcome",7,0,(struct sockaddr*)&from,len);close(fd);5 - 关闭
}

客户端:

#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>int main() { int fd = socket(AF_INET,SOCK_STREAM,0);//1-返回socket描述符if(fd == -1)perror("socket"),exit(-1);struct sockaddr_in addr;addr.sin_family = AF_INET;addr.sin_port = htons(2222);addr.sin_addr.s_addr = inet("192.168.124.10");//发送hellosendto(fd,"hello",5,0,(socket sockaddr*)&addr,sizeof(addr));//写数据//接收welcomechar buf[100] = {};resd(fd,buf,sizeof(buf));printf("%s\n"buf);close(fd);5 - 关闭
}

UDP作业
作业:
写一个基于UDP的时间服务器
时间服务器就是提供时间的服务器,有请求就回发一个时间。客户端发送一个
"hello"过来,服务器回发一个当前时间,以字符串格式(xxxx年xx月xx日xx小时xx分xx秒)
man time.h
time()返回time_t(秒差)
struct tm* localtime(time_t*)把秒差转成日期格式
struct tm是时间的结构,包括年月日小时分秒
提示:整数转字符串用springf()
时间服务器应该做成死循环,用信号退出。

服务器:

#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/n.h>
#include <arpa/inet.h>
#include <string.h>
#include <time.h>
#include <signal.h>
void fa(int signo){printf("服务器即将关闭\n");sleep(1);close(fd);exit(0);
}int main() {printf("按Ctrl+C退出\n");signal(SIGINT,fa);int fd = socket(AF_INET,SOCK_DGRAM,0);//1-返回socket描述符if(fd == -1)perror("socket"),exit(-1);struct sockaddr_in addr;//2- 通信地址addr.sin_family = AF_INET;addr.sin_port = htons(2222);//服务器的端口// htons(2222)是为了本机端口整数格式转为网络格式addr.sin_addr.s_addr = inet_addr("192.168.124.10");//服务器的地址//inet_addr("192.168.124.10")是转16进制int res= bind(fd,//3 - 开放一个端口(struct sockaddr*)&addr,sizeof(addr));if(res == -1)perror("bind"),exit(-1);printf("bind ok\n");char buf[100] = {};//4读数据struct sockaddr_in from;scoklen_t len = sizeof(from);while(1){res = recvfrom(fd,buf,sizeof(buf),0,(struct sockaddr*)&from,&len);//接收地址time_t cur_time = time(0);//秒差struct tm* cyr = localtime(&cyr_time);//转格式sprintf(buf,"%4d-%02d-%02d %02d:%02d%02d%",/*有0的原因:定长不需要清buf,变长需要清buf*/cur->tm_year+1900,cur->tm_mon+1,cur->tm_mday,cyr->tm_hour,cur->tm_min,cur->tm->sec);sendto(fd,buf,strlen(buf),0,(struct sockaddr*)&from,len);}}

客户端:

#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>int main() { int fd = socket(AF_INET,SOCK_STREAM,0);//1-返回socket描述符if(fd == -1)perror("socket"),exit(-1);struct sockaddr_in addr;addr.sin_family = AF_INET;addr.sin_port = htons(2222);addr.sin_addr.s_addr = inet("192.168.124.10");//发送hellosendto(fd,"hello",5,0,(socket sockaddr*)&addr,sizeof(addr));//写数据//接收welcomechar buf[100] = {};read(fd,buf,sizeof(buf));printf("%s\n"buf);close(fd);5 - 关闭
}

二、线程

2.1基础知识

网络是需要并行代码的,因为每个客户端都需要服务器支持。代码并行的技术就是多进程和多线程。多进程有很多的局限性,比如:分配大量的资源、进程的pid有限等。所以在网络中越来越多的使用了多线程技术实现代码并行。

线程是一种轻量级的代码并行技术,就是说对资源的要求比较少。线程隶属于某个进程,进程内部可以使用多线程,线程的内部也可以使用多线程。线程共享资源,不需要太多的额外资源,每个线程只需要额外建一个栈区即可。

同一进程内部的多线程之间互相独立,又相互影响。
每个进程的内部都至少有一个线程,叫主线程,主线程一旦结束,进程就随之结束,所有线程也就结束。
每个线程的内部代码都是顺序执行,多线程之间代码之间乱序执行。

线程并行代码的原理:
执行代码必须需要内存和CPU。我们知道内存可分,CPU不可以分。然而真正的代码并行是不存在的,举个例子:假定需要四个线程同时运行,方法是:CPU时间片机制实现伪并行,把CPU的执行时间分成极小的小片,如1毫秒。每个CPU时间片可以让一个线程拥有CPU的1毫秒执行时间,执行完毕之后忘给其他有时间片的线程执行。因此100毫秒之后4个线程分别执行了25毫秒。从宏观上看,在100毫秒时间内4个线程都执行了25次,形成了感觉上的伪并行。
多线程可以提高代码的效率,因此应用较广。

Linux/Unix关于线程的设计和实现:
Linux/Unix都在POSIX规范中有了定义,主要是有一个头文件+一个动态库。
头文件是pthread.h,共享库是libpthread.so。
所有线程相关的函数/结构/类型基本都是以pthread_开头,比如:
pthread_creat() - 创建新的进程
编译链接时,需要加 - pthread/-lpthread

所有的线程的函数,都不用errno,错误码直接返回。函数strerror()可以把错误码转换成错误信息。

2.2线程运行

int pthread_creat(pthread_t* id,pthread_attr_t* attr,void*(*fa)(void*),void* p);

参数:函数是4个指针做参数,
id用于返回线程的ID,线程ID是线程的唯一标识,但线程ID是一个比较大的整数。
attr是线程的属性,一般给0即可(默认属性)
fa是一个函数指针,将来传入线程执行函数。
p是fa的参数,线程在执行函数fa时,会把p传给fa做参数。
关于返回值:成功返回0,失败返回错误码,需要用strerror()转换错误信息。

下面写代码:

#include <stdio.h>
#include <pthread.h>
#include <string.h>void* task(void* p){int i;for(i=0;i<50;i++){printf("thread:%d\n",i);}
}
int main() {pthread_t id;//这个ID将来要收线程IDint res = pthread_creat(&id,0,task,0);if(res) printf("error:%s\n",strerror(error));/*这两行代码和task(0);是不一样的,task(0);这种写法只有一个线程先打印thread:再打印error:,是顺序运行的*//*而这两行代码的执行结果是,thread:和error:是乱序的,比如:error:0error:1error:2..error32thread:0thread:1thread:2..thread:49error:33error:34error:49这就是多线程并行线程,即并行乱序运行的,但线程内部按顺序运行这就提高了效率*/pthread_t id2 = pthread_self();//取线程自己的线程ID(这里是主线程)printf("id=%u,id2=%u\n",id,id2);int i;for(i=0;i<50;i++)printf("main:%d\n",i);sleep(1);//主线程不能在task之前结束
}

下面写完善pthread_creat()参数的代码:

#include <stdio.h>
#include <pthread.h>
#include <string.h>void* task(void* p){//自动接收到传入的参数int* pi = (int*)p;printf("*p = %d\n",*pi);*pi = 20;
}//task()真正在使用&x
int main() {int x = 10;pthread_t id;pthread_creat(&id,0,task,&x);//传一个地址sleep(1);printf("x=%d\n",x);//代码执行打印的结果是*p=10;x=20
}

练习:用线程计算一下圆的面积,半径要传入:

#include <stdio.h>
#include <pthread.h>
#include <string.h>void* task(void* p){double* pi = (double*)p;printf("s = %d\n",3.14*(*pi)*(*pi));
}
int main() {int r = 10;pthread_t id;pthread_creat(&id,0,task,&r);//传一个地址sleep(1);printf("r=%d\n",r);
}

pthread_self():取当前线程的id

上面的程序使用的sleep(1);来保证先打印面积s再打印半径r,严重浪费了时间,为此可以使用如下函数:

pthread_jion():让一个线程等待另一个线程的结束,同时可以取得结束线程的返回值。比如:在线程a写了pthread_join(b,0);那么a线程会等待b线程的结束,同时可以取得b线程的返回值。(即a阻塞,b运行)

 int pthread_join(pthread_t thread, void **retval);

参数 :thread: 线程标识符,即线程ID,标识唯一线程。
retval: 用户定义的指针,用来存储被等待线程的返回值。
返回值 : 0代表成功。 失败,返回的则是错误号

代码如下:

#include <stdio.h>
#include <pthread.h>
#include <string.h>void* task(void* p){double* pi = (double*)p;printf("s = %d\n",3.14*(*pi)*(*pi));
}
int main() {int r = 10;pthread_t id;pthread_creat(&id,0,task,&r);//传一个地址//sleep(1);pthread_join(id,0);//让task()先运行printf("r=%d\n",r);
}

下面写代码(非常规写法):

#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>void* task(void* p){int y = (int)p;//把指针当整数用printf("y=%d\n",y);
}
void* task2(void* p){int* pi = (int*)p;sleep(1);printf("*pi=%d\n",*pi);
}
int main() {int x = 10;pthread_t id;pthread_creat(&id,0,task,(void*)x);/*把整数当成指针用,因为虚拟地址本身就是一个整数*/pthread_join(id,0);//让task()先运行//以下说明要保证地址有效性int* pp = malloc(4);*pp = 100;pthread_creat(&id,0,task2,pp);free(pp);/*结果打印出来的不是100而是0(因为free()掉了但是还没有pthread_jiont()执行),这告诉我们在线程结束前必须保证地址的有效性*/pthread_jion(id,0);
}

关于函数的返回值,返回局部变量是可以的;返回局部变量的地址是不行的,但static修饰的局部变量(在全局区)可以返回地址。

int ta(){int i = 10;return i;
}//可以
int* tb(){int i = 10;return &i;
}//不可以

线程的返回值可以是任意类型的指针,pthread_join()函数可以先等待线程的结束,然后取得线程的返回值(void*类型)。

pthread_join()的第二个参数是void**类型,用于获取结束线程的返回值(void*),如果你不想取返回值,那给0即可。
对此编程如下:

#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>void* task(void* p){static int sum = 0;int i;for(i=1;i<11;i++)sum = sum + i;return &sum;}
int main() {int x = 10;pthread_t id;pthread_creat(&id,0,task,0);int* all;//和task()返回值的类型保持一致pthread_join(id,(void**)&all);//记住步骤即可printf("sum=%d\n",*all);
}

但是也可以把地址当作整数(这是非常规方案):

#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>void* task(void* p){int sum = 0;int i;for(i=1;i<11;i++)sum = sum + i;return (void*)sum;}
int main() {int x = 10;pthread_t id;pthread_creat(&id,0,task,0);int all;//和task()返回值类型保持一致pthread_join(id,(void**)&all);//取地址即可printf("sum=%d\n",all);
}

接下来讲线程的退出函数
和进程一样,线程也有自己的退出方式:
1、线程的函数执行了return语句。
2、pthread_exit(void*)退出线程,并把返回值做参数
3、线程可能被其他线程取消(非正常退出)。
注:exit()和pthread_exit()的区别:
前者是退出进程的,后者是退出线程的;
前者会导致所有线程全部结束,后者只是当前线程结束。

线程的取消:
prhread_cancel()可以取消一个线程,前提是这个线程允许你取消。

线程资源的释放和回收:
1、普通线程的资源回收不是十分的确定,没有强制回收的机制。
2、pthread_join()进来的资源会在join()函数结束时回收。
3、处于分离状态的线程在一结束的同时就回收资源,不考虑其他线程是否有关系,函数pthread_detach()可以设置线程为分离状态。
经验:一个线程,最好要么是join()的,要么是detach()的,保证回收。
一个分离的线程再去调用pthread_join()函数就没有等待的效果了。
总结起来就是:一个线程创建后,如果需要等待就调用pthread_join,不需要等的话就调用pthread_detach()。

detach.h

#include <stdio.h>
#include <pthread.h>
void* task(void* d){int i;for(i=0;i<30;i++){printf("task:%d\n",i);usleep(100000);}
}
int main(){pthread_t id;pthread_creat(&id,0,task,0);int i;for(i=0;i<30;i++){printf("main:%d\n",i);usleep(100000);//0.1s}
}//由于线程并行,main和task交叉打印,程序一共运行3s

detach.h

#include <stdio.h>
#include <pthread.h>
void* task(void* d){int i;for(i=0;i<30;i++){printf("task:%d\n",i);usleep(100000);}
}
int main(){pthread_t id;pthread_creat(&id,0,task,0);pthread_join(id,0);int i;for(i=0;i<30;i++){printf("main:%d\n",i);usleep(100000);//0.1s}
}//由于线程串行,main和task分别打印,程序一共运行6s

detach.h

#include <stdio.h>
#include <pthread.h>
void* task(void* d){int i;for(i=0;i<30;i++){printf("task:%d\n",i);usleep(100000);}
}
int main(){pthread_t id;pthread_creat(&id,0,task,0);pthread_detach(id);pthread_join(id,0);int i;for(i=0;i<30;i++){printf("main:%d\n",i);usleep(100000);//0.1s}
}//线程并行(detach使join失去作用),main和task交叉打印,程序一共运行3s

2.3 多线程通信

线程间通信:由于多线程共享地址空间和数据空间,所以多个线程间的通信是一个线程的数据可以直接提供给其他线程使用,而不必通过操作系统(也就是内核的调度)。

进程间的通信则不同,它的数据空间的独立性决定了它的通信相对比较复杂,需要通过操作系统。以前进程间的通信只能是单机版的,现在操作系统都继承了基于套接字(socket)的进程间的通信机制。这样进程间的通信就不局限于单台计算机了,实现了网络通信。

同一进程的线程share地址空间。同一进程中的线程因属同一地址空间,可直接通信。但要做好同步/互斥mutex,保护共享的全局变量。线程拥有自己的栈。同步/互斥是原语primitives.
而进程间通信无论是信号,管道pipe还是共享内存都是由操作系统保证的,是系统调用.

父子进程的派生是非常昂贵的,而且父子进程的通讯需要ipc或者其他方法来实现,比较麻烦。而线程的创建就花费少得多,并且同一进程内的线程共享全局存储区,所以通讯方便。

线程的缺点也是由它的优点造成的,主要是同步,异步和互斥的问题,值得在使用的时候小心设计。

线程间的通信方式:
1、锁机制:包括互斥锁、条件变量、读写锁
*互斥锁提供了以排他方式防止数据结构被并发修改的方法。
*读写锁允许多个线程同时读共享数据,而对写操作是互斥的。
*条件变量可以以原子的方式阻塞进程,直到某个特定条件为真为止。对条件的测试是在互斥锁的保护下进行的。条件变量始终与互斥锁一起使用。
2、信号量机制(Semaphore):包括无名线程信号量和命名线程信号量
3、信号机制(Signal):类似进程间的信号处理

线程间的通信目的主要是用于线程同步,所以线程没有像进程通信中的用于数据交换的通信机制。 这句话极为重要!!!!!!!!!

2.3线程同步

线程间的通信目的主要是用于线程同步,所以线程没有像进程通信中的用于数据交换的通信机制。 这句话极为重要!!!!!!!!!

多线程之间共享进程的资源,因此有可能出现数据的冲突。在访问共享数据时,多线程之间需要协调访问,这种技术就叫线程同步

Unix系统的线程同步技术主要包括:
1、互斥量(互斥锁)mutex
2、信号量
3、条件变量(本笔记没讲)
线程同步技术的核心思想就是在访问共享资源时,并行访问改为串行访问。但是注意了线程同步的范围不可以太大,要是全部都成串行没并行了就没啥意义了

2.3.1 线程同步技术之互斥量技术

互斥量时Unix系统官方的同步技术,也定义在了POSIX规范中,在pthread.h中。
互斥量有固定的用法和步骤:

1声明一个互斥量 pthread_mutex_t lock;
2初始化互斥量(宏定义初始化、函数初始化) 在声明的的同时用宏初始化:
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
或者在声明以后用函数初始化: pthread_mutex_init(&lock,0);
3给访问共享资源的代码上锁 pthread_mutex_lock();
4访问共享资源
5访问1完毕后记得解锁 pthread_mutex_unlock();
6如果不再使用互斥量,可以回收资源(销毁) pthread_mutex_destroy();

下面这个代码,如果要是不加线程同步这一套,就会有张飞,刘备这俩人
之间的一个人被另一个人覆盖掉的问题,也就是只打印刘备,张飞,或者是
刘备关羽。因为id1和id2这两个线程同时访问了共享资源,加上usleep(10000);
的作用,使得出现这种问题。
所以用线程同步解决,下面是代码:

newtask.c

#include <stdio.h>
#include <pthread.h>char* name[5];//字符指针数组,存多个人名(字符串)
int num = 0;//人数,也就是存放人名的下标
pthread_mutex_t lock;//1声明
void* task(void* d){pthread_mutex_lock(&lock);//3上锁name[num] = (char*)p;//放人名usleep(10000);num++;//人数自增pthread_mutex_unlock(&lock);//5解锁
}
int main(){name[num] = "liubei";num++;pthread_mutex_init(&lock,0);//2初始化pthread_t id1,id2;pthread_creat(&id1,0,task,"zhangfei");pthread_creat(&id2,0,task,"guanyu");pthread_join(id1,0);pthread_join(id2,0);//这两行让主线程不参与pthread_mutex_destroy(&lock);//6销毁int i;for(i=0;i<num;i++){printf("%s\n",name[i]);}
}

2.3.2 线程同步技术之-信号量

线程同步技术之-信号量(semaphore):

信号量是一个计数器,用于控制访问共享资源的并行线程总数
在POSIX规范中,信号量也可以用于进程的控制,但Linux并没有实现。

信号量的原理和信号量集中的信号量的原理是一样的,但就实现来说,他们没有任何关系(代码不一样)。

这里的信号量定义在semaphore.h中。信号量的使用步骤和互斥量基本类似,但信号量不是定义在pthread中。

信号量的使用步骤:

1声明sem_t sem;
2初始化sem_init();
3计数减1sem_wait();
4访问共享资源
5访问完毕,计数加1sem_post();
6回收资源sem_destroy();

当计数为1时,信号量可以实现互斥量的功能。
互斥量是官方的规范,信号量是第三方的规范,和pthread不是一个整体。

int sem_init(sem_t* sem,int which,int value);

参数: sem是信号量的指针
which就是控制进程还是线程,0代表线程,非0代表进程,目前Linux系统只支持 线程,所以给0即可。
value就是信号量的最大值。

练习:互斥量为最大值为1的信号量。

#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>char* name[5];//字符指针数组,存多个人名(字符串)
int num = 0;//人数,也就是存放人名的下标
sem_t sem;//1声明
void* task(void* d){sem_wait(&sem);//3上锁name[num] = (char*)p;//放人名usleep(10000);num++;//人数自增sem_post(&sem);//5解锁
}
int main(){name[num] = "liubei";num++;sem_init(&sem,0,1);//2初始化pthread_t id1,id2;pthread_creat(&id1,0,task,"zhangfei");pthread_creat(&id2,0,task,"guanyu");pthread_join(id1,0);pthread_join(id2,0);//这两行让主线程不参与sem_destroy(&sem);//6销毁int i;for(i=0;i<num;i++){printf("%s\n",name[i]);}
}

sem.c
(信号量的真正用法)(控制最大10个控制量)

#include <stdio.h>
#include <pthread_.h>
#include <semaphore.h>
#include <sstdlib.h>
#include <time.h>sem_t sem;
void* task(void* p){int i = (int)p;//传整数printf("第%d个线程申请连接数据库\n",i);sem_wait(&sem);//计数减一printf("第%d个线程申请成功\n",i);srand(time(0));sleep(rand()%10);sem_post(&sem);//计数加一printf("第%d个线程释放连接\n",i);
}
int main() {sem_init(&sem,0,10);//计数最大值是10pthread_t id;int i;for(i=0;i<20;i++){pthread_creat(&id,0,task,(void*)(i+1));pthread_detach(id);}sleep(19);
}

2.4 死锁

死锁:死于互斥锁。
死锁的官方定义:如果两个或多个进程永久的等待某个事件,而且该事件只能由这些等待进程的某一个引起,那么就会出现死锁状态。

避免死锁的重要原则:不要往回走
下面的线程a和线程b就是这种死于死锁的问题
线程a
pthread_mutex_lock(&lock1);

pthread_mutex_lock(&lock2);

pthread_mutex_unlock(&lock2);
pthread_mutex_unlock(&lock2);

线程b
pthread_mutex_lock(&lock2);

pthread_mutex_lock(&lock1);

pthread_mutex_unlock(&lock1);
pthread_mutex_unlock(&lock2);

三、一个综合案例

基于TCP的聊天室,使用技术TCP网络编程+多线程

基本流程-客户端先输入一个用户名,提交给服务器,然后服务器启动一个线程,
与客户进行交互。

思路:
服务器需要先定义一个描述符的数组,存每个客户的socket描述符,无论哪个客户端说话了,给所有在线的客户端发过去。客户端下线就从描述符数组中删除对应的描述符即可(清0).数组定义成长度是100即可。大家参考基于多线程的聊天室。

Unix系统 - 进程管理相关推荐

  1. Unix系统 - 内存管理

    这里写目录标题 一.关于进程的内存空间划分 二.虚拟内存地址空间的机制 三.内存管理的相关函数 3.1 关于malloc()和free()函数 3.2 关于sbrk()和brk() 3.3 关于mma ...

  2. 【k8s】理解Docker容器的进程管理(PID1进程(容器内kill命令无法杀死)、进程信号处理、僵尸进程)

    文章目录 概述 1. 容器的PID namespace(名空间) 2. 如何指明容器PID1进程 3. 进程信号处理 4. 孤儿进程与僵尸进程管理 5. 进程监控 6. 总结 参考 概述 简介: Do ...

  3. linux中daemonize用法,daemonize Unix系统后台守护进程管理软件

    在我们的工作中,很多时候都需要在linux中后台运行程序, 方法1: nohup & 方法2: daemonize Unix系统后台守护进程管理软件 优点:更加正规 后台运⾏更稳定 git c ...

  4. Android系统中的进程管理:进程的创建

    对于操作系统来说,进程管理是其最重要的职责之一. 考虑到这部分的内容较多,因此会拆分成几篇文章来讲解. 本文是进程管理系统文章的第一篇,会讲解Android系统中的进程创建. 本文适合Android平 ...

  5. linux ksh怎么查找僵尸进程,Unix 进程管理

    进程管理 如果用户在 UNIX 操作系统上执行了一个程序,那么操作系统会为这个程序创建一个运行它的特定环境.这个环境包含系统运行该程序所需的一切资源,使得好像系统中没有运行其他程序一样. 用户如果在 ...

  6. 【Linux系统编程】Linux进程管理

    00. 目录 文章目录 00. 目录 01. 概述 02. 进程相关概念 03. 进程的状态 04. 进程调度 05. 程序调度函数 06. 附录 01. 概述 在Linux的内核的五大组成模块中,进 ...

  7. web linux进程管理,详解Supervisor安装与配置(Linux/Unix进程管理工具)

    Supervisor(http://supervisord.org/)是用Python开发的一个client/server服务,是Linux/Unix系统下的一个进程管理工具,不支持Windows系统 ...

  8. linux psutil 监控,psutil:系统监测与进程管理

    简介 psutil (process and system utilities的缩写) 是一个跨平台的Python库,可以获取到运行中的进程信息和系统状态(比如CPU,内存,磁盘,网络,传感器),主要 ...

  9. 11、Linux系统基础原理、进程管理工具、任务计划

    Linux进程及作业管理 ​ 内核的功用:进程管理.文件系统.网络功能.内存管理.驱动程序.安全功能 ​ Process: 运行中的程序的一个副本: ​ 存在生命周期 Linux内核存储进程信息的固定 ...

最新文章

  1. 编译器设计-有限自动机
  2. 系统分析与设计 实验一用例模型
  3. 为什么以太坊能成为区块链2.0的代表之作?
  4. R语言ggplot2可视化多行文本轴标签、轴标签带下标、轴标签数学公式实战
  5. 关于UNION联合体结构的运用
  6. 聊聊技术、利基市场、激情与梦想
  7. (三) LtRecyclerView v2.x (自定义上拉和下拉刷新View)
  8. 2021柳州市地区高考成绩排名查询,2021年柳州所有高中排名一览表
  9. 基于JAVA+Swing+MYSQL的酒店管理系统
  10. CNN_autoencoder_mnist
  11. php点击导入选择文件,关于怎么选定一个文件并打开的有关问题
  12. windows安装MobaXterm
  13. java源程序编译命令是_java源程序编译命令是
  14. 通达OA数据库服务断电无法启动的处理方法(亲测2020-10-07。通达OA2017版本)
  15. Ubuntu20.04 vscode + opencv4教程(2021.10.27完成)
  16. Win10如何将语言栏移回右下角
  17. 我的关于 **Mermaid** 语法
  18. 什么是开源软件? 开源和FOSS解释
  19. python 批量更改扩展名
  20. python链家数据分析统计服_Python数据分析实战-链家北京二手房价分析

热门文章

  1. System32下几乎所有文件的简单说明
  2. (还在纠结Notability、Goodnotes和Marginnote吗?)iPad、安卓平板、Windows学习软件推荐
  3. DC升压高压电源模块可调直流12V24V转80V95V130V330V210V700V
  4. 纯前端实现简单的增删改查
  5. 【FinE】Girsanov定理
  6. Object.assign()详解
  7. Arduino小白教学——1.1 依次点亮小灯
  8. 《小王子》,《纳尼亚传奇:魔法师的外甥》,《鲁滨逊漂流记》短语合集
  9. STM32串口发送Ctrl+C Ctrl+Z的方法
  10. 划分网段和子网掩码 子网划分