1.创建共享库
1)编辑源程序:.c/.h
2)编译成目标模块:
gcc -c -fpic xxx.c -> xxx.o
3)链接成共享库:
gcc -shared xxx.o ... -o libxxx.so
PIC,Position Independent Code,
位置无关码。可执行程序加载共享库时,可将其映射到其地址空间的任何位置。
-fPIC - 大模式,代码量大,速度慢,所有平台都支持。
-fpic - 小模式,代码量小,速度快,仅一部分平台支持,如Linux。
2.使用共享库
1)静态加载
$ gcc main.c libmath.so
$ gcc main.c -lmath -L.
$ export LIBRARY_PATH=$LIBRARY_PATH:.
$ gcc main.c -lmath
运行时需要保证LD_LIBRARY_PATH环境变量
中包含共享库所在的路径。
2)动态加载
#include <dlfcn.h>
A.加载共享库
void* dlopen (
const char* filename,
int flag
);
filename:若只给共享库文件名,则通过
LD_LIBRARY_PATH环境变量搜索共享库。若给共享库路径,则按照路径加载,不使用环境变量。
flag:
RTLD_LAZY - 延迟加载,什么时候使用共享库再实际载入之。
RTLD_NOW - 立即加载。
成功返回共享库句柄,失败返回NULL。
B.获取函数地址
void* dlsym (
void* handle, // 共享库句柄
const char* symbol // 函数名
);
成功返回函数地址,失败返回NULL。
C.卸载共享库
int dlclose (
void* handle // 共享库句柄
);
成功返回0,失败返回非零。
D.获取错误信息
char* dlerror (void);
返回错误信息字符串,没有错误信息返回NULL。
gcc main.c -ldl
二、几个辅助工具
nm:查看目标文件、可执行文件、静态库、共享库中符号列表。
ldd:查看可执行程序或共享库的动态依赖。
ldconfig:事先把共享库的路径信息写到
/etc/ld.so.conf配置文件中,ldconfig根据该配置文件生成/etc/ld.so.cache缓冲文件,并将该缓冲文件读入内存,提高动态库的加载效率。系统启动时自动执行ldconfig,若修改了共享库配置,则需要手动执行该程序,更新缓冲。
strip:通过删除符号表和调试信息,给目标文件、可执行文件、库文件减肥。
objdump:对机器指令做反汇编。
三、错误处理
1.通过函数的返回值表示错误
1)返回合法值表示成功,返回非法值表示失败。
2)返回有效指针表示成功,返回空指针(NULL/0xFFFFFFFF)表示失败。
3)返回0表示成功,返回-1表示失败,如果有需要返回给调用者的数据,可以通过指针型参数向其输出。
4)如果一个函数永远不会失败,也没数据需要提供给调用者,可以没有返回值。
2.通过错误码获得函数失败的原因
#include <errno.h> // extern int errno;
1)通过errno全局变量获取出错原因。
2)将errno转换为一个字符串:
#include <string.h>
char* strerror (int errnum);
#include <stdio.h>
void perror (const char* s);
printf ("%m");
所有的错误码都非零,errno == 0表示无错误。
3)errno在函数执行成功的情况下不会被修改,因此不能以errno非零作为发生错误的判断依据,除非在调用函数前人为将其复位为0。
4)errno是一个全局变量,其值随时有可能发生变化,线程不安全。
四、环境变量
1.环境表
1)每个进程都会接收到一张环境表,
是一个以NULL指针结尾的字符指针数组。
2)全局变量environ保存了环境表的首地址。
3)main函数的第三个参数就是环境表的首地址。
2.环境变量函数
#include <stdlib.h>
环境变量:<name>=<value>
getenv - 根据name获得value
putenv - 以<name>=<value>形式设置
环境变量。如果name不存在,就
添加,存在修改原来的value
setenv - 根据name设置value,若name
以存在,根据参数决定是否覆盖原
value
unsetenv - 删除环境变量
clearen - 清空环境变量,environ == NULL
五、内存管理
Boost/ACE/MFC/...
STL:内存分配器
C++:new/delete,构造/析构
标C:malloc/calloc/realloc/free
POSIX:brk/sbrk
Linux:mmap/munmap 用户层
-----------------------------------------------
内核:kmalloc/vmalloc 系统层
驱动:get_free_page
硬件实现
六、进程映像
1.程序是保存在磁盘上的可执行文件,
如:a.out、ls、gcc、qq.exe。
2.运行程序时,需要把磁盘上的可执行文件,加载到内存中,形成进程。
3.一个程序(文件)可以同时存在对个进程(内存)。
4.进程在内存空间中的布局就是进程映像。从
低地址到高地址依次是:
代码区(text):可执行指令、字面值常量、具有常属性且初始化的全局和静态变量。只读。
数据区(data):不具常属性且初始化的全局和静态变量。
BSS区(bss):未初始化的全局和静态变量。
进程一加载此区即被清0。
堆区(heap):动态内存分配。
栈区(stack):非静态局部变量。
命令行参数和环境变量区
在堆区和栈区之间会留有一段空隙,
一方面为堆和栈的增长预留空间,
同时共享库、共享内存也会占用这个区域。
七、虚拟内存
1.每个进程都有各自独立的4G字节的虚拟地址空间。
2.用户程序中使用的都是虚拟地址空间中的地址,永远无法直接访问实际物理内存地址。
3.虚拟内存到物理内存的映射有操作系统动态维护。
4.虚拟内存一方面保护了操作系统的安全,另一方面允许应用程序使用比实际物理内存更大的地址空间。
5.4G的进程空间分为两部分,0~3G-1为用户空间,3G~4G-1为内核空间。
6.用户空间中代码不能直接访问内核空间中的代码和数据,但是可以通过系统调用进入内核态,间接地与内核交互。
7.对内存的越权访问,或访问未建立映射的虚拟内存,将会导致段错误。
int* p;
*p = 100;
--------------
int a;
int* p = &a;
*p = 100;
--------------
int* p = malloc (sizeof (int));
*p = 100;
Unix内存管理的函数:
malloc() free() - 标C函数
sbrk() brk() - Unix的系统函数
mmap() munmap() - Unix的系统函数
malloc申请内存时,如果是小块内存,一次映射33个内存页(物理内存),分配申请的数量(虚拟内存)。
malloc()除了分配申请的内存之外,还需要额外的空间存储附加数据。
如果申请的是大块内存,一次映射比申请的稍多的内存页,分配申请的数量。
sbrk()和brk()是Linux的系统函数,本身具备分配和回收内存的能力。sbrk()分配内存简便,brk()回收内存方便。sbrk()、brk()没有额外的附加数据,也没有33页的映射。一次就是1页,底层维护一个位置,用位置的变化分配和回收内存。
mmap()和munmap()在内存分配/回收时提供了更多的选择,是一个可管理的内存分配方式。可以用第一个参数设定分配的首地址,也可以用第三个参数设定权限,第四个参数包括:
MAP_SHARED/MAP_PRIVATE 设定是否共享(只对映射文件有效)。
MAP_ANONYMOUS 设定映射物理内存
默认情况下,mmap()映射文件。
今天:
man 手册能查什么?
Unix/Linux命令
函数
头文件
系统调用 - 因为用户空间不能直接访问内核空间,想完成功能又必须得到内核的支持。因此,内核层提供了系统调用,做用户空间进入内核空间的桥梁。系统调用是 一系列的函数,包括各种系统的功能。以后我们接触的大多数都是系统调用。
文件操作 - 非常常用的函数,包括:读写函数和 非读写函数。
在Linux系统中,几乎一切都是文件。目录、内存、各种硬件设备都可以看成文件。比如:/dev/tty 代表键盘和显示器。
echo hello 默认输出到显示器上
echo hello > a.txt 把输出改到a.txt中
echo hello > /dev/tty 把输出改到显示器中
cat /dev/tty 直接从键盘读数据
ctrl+C 退出
vi ../ 查看上层目录的内容
标C用FILE*(文件指针)代表一个打开的文件,UC用文件描述符代表一个打开的文件。文件描述符其实就是一个非负整数,文件描述符自身不存储任何文件信息,信息都存在 文件表中,文件描述符对应文件表。对应Linux来说,一个进程最多同时打开256个文件,描述符从0开始计算。0、1、2系统已经占用,程序员不能使用,代表标准输入、标准输出和标准错误。程序员的文件描述符从3开始。
文件读写函数:
open() read() write() close() ioctl()
open() - 打开一个文件,返回文件描述符
read()/write() - 读/写一个文件
close() - 关闭文件
int open(char* filename,int flags,...)
参数: filename 是打开文件的路径(包括文件名)
flags 标识,主要有以下宏定义:
权限标识:O_RDONLY O_WRONLY O_RDWR
权限标识必选其一
附加标识: O_APPEND 用追加的方式打开(从文件尾开始写,读文件一般不用)
创建标识:
O_CREAT 存在就打开,不存在就新建 O_TRUNC 文件存在时清空所有数据(谨慎)
O_EXCL 不存在就新建,存在不打开,而是返回-1,代表出错。
第三个参数 ... 叫可变长参数,代表0-n个任意的参数,只有在新建文件时,才使用。传入新文件的权限。
注: 第三个参数是文件在硬盘上的权限(某些权限可能被系统屏蔽)。O_RDONLY等是文件描述符的权限。
返回文件描述符,失败返回-1。
多个选项 用 位或 | 连接。
int read(int fd,void* buf,size_t size)
int write(int fd,void* buf,size_t length)
参数:fd文件描述符,就是open()的返回值
buf是读/写的首地址,任意类型都可以
size是buf的大小(有可能读不满)
length是真实想要写入的字节数(满)
返回有三种:
正数 - 真实读到/写入的字节数
0 - 读到文件尾/什么都没写
-1 - 出现错误
注: read()返回0 通常用于循环读文件的退出。
vi编辑器用wq保存退出时,自动加一个结束符,可以被cat换行,但是用write()没有加。
关于字符串的处理
C程序员定义字符串有三种:
"abc" 字面值,本身不是变量
char buf[length]; 字符数组
char* st ; 字符指针
字符串以'\0'做结尾。
具体操作见 string.c
数组可以看成常指针(不能改地址,只能初始化),某些时候和指针有区别(比如sizeof)。
问题:
读写文件用哪套?标C还是UC函数?
如果考虑通用性,用标C的。但如果确定只在Unix/Linux中使用,UC的也没问题。
time a.out可以查看a.out的运行时间
所有的标C函数都在用户层定义了输入/输出缓冲区,作用就是累计到一定量以后再进入内核读/写一次。UC函数都没有定义缓冲区,但可以由程序员自定义缓冲区提升效率。
文件读写的位置用偏移量记录,在文件表中存储了偏移量。函数lseek()可以随意移动偏移量。
int lseek(int fd,int offset,int whence)
参数:fd 就是文件描述符
offset 是偏移量
whence是偏移的开始位置
返回当前位置到文件头的偏移量,失败返回-1.
注:
whence + offset可以确定位置,whence包括:
SEEK_SET - 从头开始
SEEK_CUR - 从当前位置开始
SEEK_END - 从结尾开始
lseek()的返回值可以计算文件的大小。
文件操作的非读写函数
1 dup() dup2() - 复制文件描述符,但不复制文件表
dup()由系统选定新描述符的值,dup2()由程序员指定新描述符的值,如果指定值已经被使用,先关闭再使用(不安全的隐患)。
内存用虚拟内存地址管理,硬盘上的文件/目录如何管理,用inode管理。i节点可以认为是文件在硬盘上的地址。ls -i可以查看文件/目录的i节点。
函数fcntl()实现很多的功能,由参数cmd决定,常见的应用:
1 可以复制文件描述符
2 可以设置/获取描述符的状态
3 可以设置文件锁
int fcntl(int fd,int cmd,...)
参数: fd 文件描述符
cmd 命令,可以设置fcntl()完成什么功能,常用功能:
F_DUPFD(long) - 复制文件描述符,传入第三个参数做新描述符的值。和dup2()的区别在于不会强行关闭已使用的描述符,而是寻找大于等于参数的最小未使用的值。
F_SETFL(long)/F_GETFL(void) - 设置/获取描述符的状态,比如权限。其中,设置时,只对O_APPEND有效,权限和创建标识都无效。获取时,只能取权限和O_APPEND,创建标识取不到。
F_SETLK/F_SETLKW/F_GETLK - 文件锁的操作
位与 运算用于 取某一位或者取某几位,比如取后8位:
int a;
取后8位: a & 0xFF
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
//字符串的常见操作代码
int main(){
//1 赋值(=改地址,而strcpy()改值)
char* s1 = "abc";
char s2[10] = "abc";//数组是常指针
s1 = "123";//Y
//strcpy(s1,"123");//修改只读区 段错误
//s2 = "123";//常指针不能改地址
strcpy(s2,"123");//值可以改
char* s3 = malloc(10);
strcpy(s3,"abc");//Y
//s3 = "123";//错 改地址,指向了只读区
printf("s3=%s\n",s3);
//free(s3);//free()失败,内存泄露
//2 字符串的长度strlen(),buf大小sizeof
char buf[50] = "abc";
printf("size=%d,length=%d\n",
sizeof(buf),strlen(buf));
//3字符串的比较(==比的是地址,strcmp比值)
char s4[10] = "abc";
char s5[10] = "abc";
printf("%d\n",strcmp(s4,s5));
printf("%d\n",s4==s5);
//4 用指针操作字符串
char* s6 = "zhangfei,123";
int len = strlen(s6);
char name[len];
char passwd[len];
memset(name,0,len);//清空name
memset(passwd,0,len);
int i,flag=0,pos=0;
for(i=0;i<len;i++){
if(*(s6+i)==',') {//遇到,
flag = 1; pos = i; continue;
}
if(!flag) name[i] = *(s6+i);
else passwd[i-pos-1] = *(s6+i);
}
printf("name=%s;pwd=%s\n",name,passwd);
//5 字符串的拼接
char fpath[20] = "/home/tarena";
char fname[20] = "a.txt";
char pname[50] = {};
/*strcpy(pname,fpath);
strcat(pname,"/");
strcat(pname,fname);*/
sprintf(pname,"%s/%s",fpath,fname);
printf("pname=%s\n",pname);
//6 字符串和其他类型的转换
//sprintf()和sscanf()
int x = 100;
char bufx[10] = {};
sprintf(bufx,"%d",x);//其他类型转字符串
printf("%s\n",bufx);
int y;
sscanf(bufx,"%d",&y);
printf("y=%d\n",y);
}
--------------------------------------------------------------------------------------------------------------------------------------------------------------
回顾:
文件相关函数 - lseek() dup() fcntl()
String.c - C程序员的基本功
今天:
当多个进程同时写一个文件时,有可能引发数据混乱,这个问题需要解决。解决方案包括:进程之间的同步 或 文件锁。文件锁就是当一个进程读写文件时,对其他进程进行读写的限制。
结论:一个进程读,允许其他进程读,但不允许其他进程写。
一个进程写,其他进程不能读也不能写。
文件锁是一个读写锁,包括读锁和写锁。
读锁是一个共享锁,允许其他进程读(共享),不允许其他进程写(锁)。如果进程是读文件,就应该上读锁。
写锁是一个互斥锁,不允许其他进程读/写(互斥锁)。如果进程是写文件,就应该上写锁。
fcntl(fd,cmd,...)
当cmd为F_SETLK/F_SETLKW时,可以对文件上锁。
当使用文件锁时,第三个参数就是结构体flock指针。
struct flock{
short l_type;//锁的类型
short l_whence;//锁定起始点的参考位置
off_t l_start;//针对参考位置的偏移量
off_t l_len;//锁定的区间长度
pid_t l_pid;//只对F_GETLK有效,一般给-1
};
锁的类型包括:F_RDLCK(读锁) F_WRLCK(写锁)
F_UNLCK(释放锁)
l_whence和l_start联合决定了锁定的起始点。比如:l_whence选SEEK_SET,l_start为10,就是从头开始偏移10个字节以后开始锁。
进程结束自动释放文件锁,但最好还是程序员自己释放。
文件锁只是内存中的一个标识,不会真正锁定文件。fcntl(F_SETLK)不能锁定read()/write(),只能锁定其他进程的加锁行为fcntl(F_SETLK)。文件锁的正确用法是:
在调用read()函数之前用fcntl()加读锁,能加上再读,读完以后释放读锁;在调用write()之前用fcntl()加写锁,能加上再写,写完以后释放写锁。
fcntl(fd,F_SETLK,&读锁);
read(fd,...);
fcntl(fd,F_SETLK,&释放锁);
或:
fcntl(fd,F_SETLK,&写锁);
write(fd,...);
fcntl(fd,F_SETLK,&释放锁);
但不管怎么加锁,类似vi的编辑器是无法锁定。
F_SETLK当锁加不上时,直接返回-1,而F_SETLKW当锁加不上时,会继续等待,等到能加上为止。
F_GETLK不是获得当前的锁,而是测试一下某个锁能不能加上,并不真正的加锁。(了解)
C语言中,参数可以有三种:
传入型参数 - 给函数传值,比如: add(int,int)
传出型参数 - 带回函数的结果,一般是指针类型
传入传出型参数 - 先传入一个值,再带出一个值
函数的返回值,可以用return直接返回,也可以用传出型参数返回。
stat()就是用传出型参数返回文件的信息。
stat()可以取得文件的以下信息:
ls -il 的所有信息,其中最常用的是st_size。
st_mode需要拆分,文件类型和权限。
access()可以判定当前用户对文件的权限和文件是否存在。
int access(char* fname,int mode)
参数fname就是带路径的文件名
mode 就是判断什么,包括:
R_OK - 读权限
W_OK - 写权限
X_OK - 执行权限
F_OK - 文件是否存在
返回0代表有权限或者文件存在。
其他函数:
chmod() - 修改文件的权限
chmod("a.txt",0666)
truncate()/ftruncate() - 指定文件的大小
truncate("a.txt",100)
remove() - 删除文件/空目录
rename() - 文件改名
umask() - 修改创建文件时,系统默认的权限屏蔽字。默认屏蔽其他用户的写权限 - 0002。umask()可以修改默认的权限屏蔽字(只针对新建文件)。
mode_t
umask(mode_t)
传入新的权限屏蔽字,返回之前的权限屏蔽字,用于处理之后的恢复。
mmap() 可以映射物理内存,但也可以映射文件,默认情况下 映射文件,映射物理内存需要加MAP_ANONYMOUS标识。
目录相关函数:
mkdir() - 新建一个目录
rmdir() - 删除一个空目录
chdir() - 切换当前目录 (cd)
getcwd() - 取当前目录(返回绝对路径) 双返回
char* s = getcwd(0,0);
读目录的函数:
opendir() - 打开一个目录,返回目录流(指针)
readdir() - 读目录的一个子项(子目录/子文件)
效果相当于 ls 目录
closedir() - 关闭目录流(不写也可以)
使用递归的必要条件:
1 使用递归以后,问题简化而不是复杂
2 递归必须有 退出条件
3 使用递归要注意效率问题。
回顾:
文件和目录的相关函数
fcntl() 可以使用文件锁,阻止多个进程同时操作文件带来的问题。
struct flock lock;
//l_type l_whence l_start l_len l_pid
stat() - 获取文件的属性/信息(硬盘上的文件)
chmod() - 改权限
truncate() - 改文件大小
remove() rename() - 删除 改名
access() - 获取文件的权限/判断文件是否存在
目录的相关函数
mkdir() rmdir() - 新建目录 删除空目录
chdir() - 切换当前目录
getcwd() - 取当前目录的绝对路径格式
opendir() readdir() closedir() - 读目录
今天:
程序和进程
程序就是代码编译连接的成品(a.out),程序是硬盘上的文件。
进程就是运行在内存中的程序,一个程序可以启动多次,得到多个进程。
CPU(中央处理器)只能直接操作内存,不能直接操作硬盘的。硬盘上的程序要想运行,先加载到内存中去,就变成了进程。
有些时候也把进程叫程序。
主流的操作系统都是多进程的,每个进程内部可以用多线程实现功能的并行(同时运行)。
进程相关命令(Unix版):
ps : 只能看到当前终端启动的程序
ps -aux : Linux专用查看所有进程的命令,Unix不直接支持。
ps -ef : 通用版
kill -9 进程ID : 杀进程(发信号)
常见的管道用法:
管道的作用就是用前面的输出作为后面的输入
ps -ef | wc - 统计行数、字节数等
ls -al | more - 分页显示(空格 回车 q)
Unix/Linux系统由 内核和SHELL,SHELL主要有: sh/bash(sh的升级版)/csh
whereis XXX 可以查看文件名XXX在哪里
父进程和子进程
如果a进程启动了b进程,a就是父进程,b就是子进程。Unix/Linux系统的启动次序是:系统启动0进程,0进程启动进程1/进程1和进程2,其他进程都由进程1/进程1和进程2启动。
进程的状态
每个进程都有自己的状态,主要包括:
S - 休眠状态,大多数进程处于休眠状态
s - 有子进程
R - 正在运行
Z - 僵尸进程(已经结束,但资源没有回收)
T - 暂停或被追踪
每个进程用进程ID(PID)做唯一标识,进程PID是系统管理。函数getpid()可以取得进程的PID。如果进程结束,PID是可以重复使用,但要延迟重用。PID唯一标识一个进程。
getpid() - 取当前进程的PID
getppid() - 取父进程的PID
getuid() - 取当前用户的ID。
如何创建子进程?
fork() - 非常复杂的简单函数,通过复制父进程创建子进程。
vfork()+execl() - 不复制任何东西,创建一个全新的子进程。
进程PID用pid_t类型,是一个非负整数。
pid_t fork() , 返回子进程的PID或0,出错 -1。
fork()是通过复制父进程的内存空间创建的子进程,除了代码区父子进程共享(只读),其他内存区域子进程都要复制。
fork()创建子进程之后,父子进程同时运行,但谁先运行不确定,谁先结束也不确定。
fork()在复制父进程的内存空间时,如果遇到文件描述符,复制描述符但不复制文件表。
fork()在复制父进程的内存空间时,也会复制输出/输入缓冲区。
fork()函数调用一次,返回两次。父进程返回一次,子进程也会返回一次。父进程返回子进程的PID,子进程返回0。
关于父进程的运行和资源回收:
父进程启动子进程后,父子进程同时运行。如果子进程先结束,会给父进程发信号,父进程负责回收子进程的资源。
父进程启动子进程后,父子进程同时运行。如果父进程先结束,子进程变成孤儿进程,认进程1(init)做新的父进程,init进程也叫 孤儿院。
父进程启动子进程后,父子进程同时运行。如果子进程没有给父进程发信号就结束,或者父进程没有及时处理信号,此时子进程就变成僵尸进程。
fork()之前的代码父进程执行一次,fork()之后的代码父子进程分别执行一次,fork()将返回两次。
刷新输出缓冲区的条件:
1 遇到换行 \n
2 缓冲区满了
3 程序结束了
4 fflush()函数人工刷新
进程结束的方式分为正常结束和非正常结束。正常结束包括:
主函数中执行了return
执行exit()
_Exit()和_exit()
所有线程都结束
非正常结束:
信号打断进程(ctrl+c、kill -9)
最后一个线程被取消
exit() 与 _Exit()/_exit()的区别:
_Exit()/_exit()基本无区别,都是立即退出进程。
exit()不是立即退出,甚至可以先执行在 atexit()中注册函数后再退出。
函数wait()/waitpid()可以让父进程等待子进程结束,并取得子进程的退出状态和退出码(return/exit(值))。
pid_t wait(int* status)
wait() 函数 让父进程等待任意一个子进程的结束,并返回结束子进程的PID,把结束子进程的退出状态和退出码存入status中。如果没有子进程结束,会阻塞父进程,直到有子进程结束为止。包括僵尸子进程,因此wait()也叫殓尸工。
宏函数WIFEXITED(status)判断是否正常结束,而WEXITSTATUS(status) 可以获得退出码。
回顾:
进程 - 进程的概念、基本操作、fork()、getpid()
getppid()
进程相关的命令: ps/kill/whereis
wait() exit()/_Exit()
fork()通过复制自身创建子进程,复制除了代码区之外的所有区域。但遇到文件描述符时,只复制描述符,不复制文件表。
wait() 主要让 父进程等待子进程的结束(包括僵尸子进程),如果没有子进程结束,父进程将阻塞,直到有子进程结束为止。
pid_t wait(int* status)
WIFEXITED() / WEXITSTATUS()
今天:
waitpid() 可以设置等待的方式和等待的子进程。
pid_t waitpid(pid_t pid,int* status,
int options)
参数:pid可以指定等待哪个/哪些子进程
status用法和wait一样
options可以设置非阻塞的等待(不等待)
options 为0 , 没有子进程结束继续等待
为WNOHANG,没有子进程结束不等待,直接返回0.
pid的值:
-1 : 等待任意子进程,和wait()一样
>0 : 等待子进程的ID=pid(特指)
0 : 等待本组子进程(与父进程相同进程组)
<-1: 等待进程组为|pid|的所有子进程
注:-1 和 >0 常用,后面两个了解即可。
返回有三种可能:
结束子进程的pid
-1 代表出错
0 只有在options为WNOHANG时可能返回,代表没有子进程结束,也没有出错。
fork() 父子进程是使用相同的代码区,如果 需要父子进程代码区不同的话,可以使用 vfork()+execl()。
vfork() 创建新的子进程,execl()负责提供子进程的代码和数据(程序)。
execl()函数是用新的程序替换原有的程序。
vfork() 从语法上和fork()完全一样,区别在于vfork()不复制任何父进程的资源。vfork()会抢父进程的资源,导致父进程阻塞。父进程解除阻塞的条件:
1 子进程结束时,归还父进程的资源(无并行)。
2 子进程调用exec系列函数(execl等),也归还父进程资源。
注意:
vfork()确保子进程先运行(父进程没资源),调用execl()之后父子进程同时运行。
vfork()创建的子进程必须用exit()退出。
execl()可以用一个新程序替换旧程序,但不新建任何的进程。如果新的程序正常启动,旧程序不再继续运行;如果新的程序启动失败,旧程序继续运行。
execl(程序所在的路径,命令,选项,命令参数,NULL)
启动失败返回-1.
信号(signal)
信号是Unix/Linux系统中 软件中断的最常用方式
中断是什么?
中断就是中止当前正在执行的代码,转而执行其他代码。
中断分为软件中断和硬件中断。
常见的信号:
ctrl+c
段错误
总线错误
整数除以0
kill -9 发信号9
子进程结束,给父进程发信号
信号本质就是一个非负整数,Unix和Linux在信号上有区别,Unix是48个,Linux是64个,但中间不保证连续。
每个信号都有一个 宏名称,编程时尽量使用宏名称而不是信号的值。不同的系统中,同一个宏名称对应的值可能不同。宏名称 以 SIG开头。比如:
SIGINT 就是 信号2的宏名称
查看信号都有哪些,可以使用kill命令。发送信号也可以使用kill命令。
kill -l : 查看所有信号
kill -整数 : 发送信号
信号分为可靠信号和不可靠信号,1-31 都是 不可靠信号,34-64都是可靠信号。不可靠信号 不支持排队,因此如果有多个 相同的不可靠信号同时到来时,可能出现信号丢失。可靠信号支持排队,因此不会丢失。
信号的处理方式:
1 默认处理 - 系统对每个信号都有默认处理方式,默认处理大多数都是 退出进程。
2 忽略信号 - 不做任何的处理,就像没有信号一样。
3 自定义信号处理函数 - 信号的处理方式改为执行我们自己定义的函数。
注:
信号9 不能忽略,也不能自定义处理函数。
当前用户只能给当前用户的进程发信号,不能给其他用户的进程发信号。 root可以给所有进程发信号。
信号0 没有特殊的意义,用于 测试是否有发信号的权限。 kill -0 3333(测试对3333进程是否有发送信号的权限)
Unix系统提供了设置信号的处理方式的函数,signal()、sigaction()。
( void (*f) (int) ) signal(int signum,
void (*f) (int))
参数 signum就是被设置处理方式的信号
第二个参数是函数指针,支持三种值:
SIG_IGN - 代表忽略该信号
SIG_DFL - 代表信号到来执行默认处理方式
自定义的函数 - 代表信号到来执行自定义函数
返回之前的信号处理方式,如果出错返回 SIG_ERR.
自定义信号处理方式的步骤:
1 写一个处理函数,格式 void fa(int){ }
2 调用signal(int signum,fa)注册处理函数。
如果父进程改变了信号的处理方式,子进程如何?
fork()创建的子进程,与父进程的处理方式一致
vfork()+execl()创建的子进程,父进程忽略的,子进程也忽略;父进程默认的,子进程也默认;父进程自定义处理函数,子进程改为默认。
killall可以删除所有同名的进程,比如:
killall a.out 就会删除所有的a.out进程
信号的发送:
1 用键盘发送信号(部分)
ctrl+c -> 信号2
ctrl+\ -> 信号3
ctrl+z -> 信号20
2 硬件故障/或者程序出错(部分)
段错误、总线错误、整数除0
3 kill命令发送信号(全部)
kill -信号 进程PID
4 信号发送函数(全部)
kill()、raise()、alarm()、sigqueue()等
int kill(pid_t pid,int signum)
参数 pid 就是发送哪个/哪些进程,使用方式和waitpid(pid)一样。
signum就是发送哪个信号
成功返回0,失败返回-1.
发送信号时,一般pid 为正数,也就是发给特定进程。
回顾:
进程waitpid(),等待子进程的结束。等待方式可以有多种选择。
vfork()+execl() 启动子进程,这种方式不做内存的复制,而是用execl()启动全新的程序。
信号 - 信号本质是个非负整数,用于 软件中断。信号分为可靠信号和不可靠信号,可靠信号支持排队,不会丢失,34-64都是可靠信号。不可靠信号不支持排队,会丢失,1-31都是不可靠信号。
信号处理方式有三种:
默认处理,一般都是退出进程。
忽略信号,不做任何的处理。
自定义处理函数,用signal()函数注册处理函数
子进程对父进程的信号处理方式有继承,fork()是完全继承,vfork()+execl()是部分继承,自定义处理函数的会改为默认,其它的不变。
信号发送函数: kill() ,用法和kill命令类似,但功能更强。
void* fa(int) -> 函数声明
void (*a)(int) -> 函数指针
今天:
alarm()函数 - 不是真正意义的信号发送函数,而是过一段时间(秒数)发送特定的信号。
sleep() - 让程序休眠一段时间(秒数),但可能被非 忽略的信号打断。
usleep() - 让程序休眠一段时间(微秒).
信号集和信号屏蔽
long long int - C语言的64位整数
多个信号可以存入信号集,类型sigset_t,可以看成一个超大型整数。
数据结构包括: 逻辑结构、物理结构和运算结构。逻辑结构就是逻辑上是怎样的(人脑中的定义),物理结构就是内存如何组织的(计算机底层的实现),运算结构就是需要对外提供什么函数(实现的功能)。
运算结构主要包括:
1 创建和销毁函数
2 增加元素和删除元素
3 修改元素和查询元素
4 其他函数,比如排序。
信号集的函数:
1 增加信号和删除信号(分单独和全部)
2 查询信号
sigaddset() - 增加一个信号(二进制位 置1)
sigdelset() - 删除一个信号(二进制位 置0)
sigemptyset() - 全部删除信号
sigfillset() - 填满全部信号
sigismember() - 查询有没有某个信号
信号屏蔽
信号不确定什么时间会来,因此有可能在非常重要的场合(执行关键代码)信号到来,此时可能产生重大的错误。程序员无法阻止信号的到来,但是可以屏蔽信号,就是信号可以到来但暂时不做处理,等关键代码执行完毕,解除信号屏蔽后再做处理。
信号屏蔽/解除函数 sigprocmask()
int sigprocmask(int how,sigset_t* set,
sigset_t* old)
参数:how就是信号屏蔽的方式,包括:
SIG_BLOCK - 相当于旧的屏蔽+新的屏蔽
A B C + C D E -> A B C D E
SIG_UNBLOCK - 相当于旧的屏蔽 - 新的屏蔽
A B C - C D E -> A B
SIG_SETMASK - 就是无视旧的,直接替换成新的屏蔽。
set 就是新的权限屏蔽字
old是一个传出参数,可以传出旧的权限屏蔽字,用于恢复之前的屏蔽
注: 信号屏蔽之后一定要解除屏蔽。
一般情况下,how都采用SIG_SETMASK。
信号9 屏蔽无效。
函数sigpending()可以判断在信号屏蔽期间,有没有信号来过。功能就是把信号屏蔽期间来过的信号放入信号集。
sigaction() (了解)
sigaction()也是一个信号处理方式的注册函数,是signal()的增强版,sigaction()可以拿到更多的信号相关信息,甚至可以在发送信号的时候附带其他的数据。sigaction()中,信号的处理函数支持两种格式: signal()的格式和更复杂的格式。
C语言中,结构里面可以写函数么?
不可以,但C++可以。
C语言中,如果结构中需要函数,可以使用函数指针做成员。
信号的应用之计时器。
每个进程在Linux中都有三种计时器,真实计时器、虚拟计时器和实用计时器。其中真实计时器是产生SIGALRM工作。计时器可以用setitimer()进行设置。
int setitimer(int which, const struct itimerval *value, struct itimer val *ovalue);
参数which选择哪种计时器,一般都是真实计时器。
struct itimerval 设置计时器的开始时间和间隔时间。
进程间通信 - IPC
Unix/Linux系统基于多进程,进程和进程之间经常做数据的交互,这种技术叫进程间通信。
常见的IPC:
1 文件
2 信号
3 管道
4 共享内存
5 消息队列
6 信号量集
7 网络编程(socket)
...
其中,管道是最古老的的IPC之一,目前较少使用。共享内存、消息队列和信号量集 遵循相同的规范,因此编码上有很多的共同点,并且这三个统称为XSI IPC。网络编程以前用于IPC,现在更多的用于网络。
管道(pipe) - 就是用管道文件做交互媒介的IPC。管道文件是一种特殊的文件,ls时 文件类型是p。
mkfifo命令/函数 管道文件名 就可以创建管道文件。touch命令和open() 都无法创建管道文件。
管道文件只是交互的媒介,不存储任何的数据;只有在有读进程 有写进程时 才能畅通,否则阻塞。
管道有两种用法: 有名管道和无名管道。
有名管道可以用于所有进程之间的交互,而无名管道只能用于fork()创建的父子进程之间的交互。
有名管道就是由程序员创建管道文件进行IPC。无名管道就是系统创建和维护管道文件进行IPC。
有名管道的用法:
1 用mkfifo命令/函数 创建管道文件。
2 像读写普通文件一样操作管道文件。
3 如果不再使用管道文件,可以删除。
练习:
使用有名管道 实现 pipea.c 和 pipeb.c 之间的IPC。pipea 发送100个整数给pipeb。
回顾:
信号集、信号屏蔽、sigaction()
IPC - 管道、(文件、共享内存、消息队列、信号量集、网络编程)
信号集类型是sigset_t,是一个超大的整数。信号集用来存储多个信号,使用函数:
sigaddset() sigdelset() sigfillset()
sigemptyset() sigismember()
信号屏蔽 就是在执行关键代码时,不希望被信号打断,因此使用信号屏蔽不是阻止信号的到来,但是可以暂时不做处理,直到关键代码执行完毕,解除了屏蔽以后才处理。sigprocmask()
管道就是用 管道文件 做交互媒介的 IPC。
mkfifo命令/mkfifo() 可以创建管道文件
今天:
XSI IPC 之 共享内存、消息队列
XSI IPC 包括共享内存、消息队列和信号量集,遵循相同的规范。
标准(规范) 、 产品 和 项目
标准是行业准则,任何相关软件都必须遵守。标准是 行业共同协商的成果。做标准的公司最幸福的。
产品就是遵循标准的软件,产品更注重质量,不是为个别客户服务的。比较轻松,不用特别赶时间
项目是针对 特定客户的定制,客户的影响力非常大,时间一般比较紧张。比较累,而且需要年轻化
XSI IPC的通用规范:(三种都可以用)
1 所有的IPC结构都有一个内部的ID做唯一标识
2 内部ID的获取 需要借助 外部的key,类型key_t。
3 key的获取有三种方式:
a 使用宏 IPC_PRIVATE做key,但这种方式外部无法获取,因此基本不用。
b 使用ftok()提供一个key。
c 在头文件中统一定义所有的key。
4 用key获取内部id的函数都是 xxxget(),比如: shmget() 、msgget()
5 每种IPC结构都提供了一个 xxxctl()函数,这个函数的功能至少包括:
查询、修改和删除。
其中有一个cmd参数,值:
IPC_STAT - 查询
IPC_SET - 修改
IPC_RMID - 按ID删除IPC结构
6 所有IPC结构都是内核管理,不使用时需要手工删除。
7 IPC结构的相关命令:
ipcs 查询当前的IPC结构
ipcrm 删除当前的IPC结构(用id删除)
选项: -a 所有IPC结构
-m 共享内存
-q 消息队列(更常用)
-s 信号量集
共享内存:
以 一块共享的物理内存做媒介。通常情况下,两个进程无法直接映射相同的内存。共享的实现:
1 内核先拿出一块物理内存,内核 负责管理。
2 允许所有进程对这块内存进行映射。
3 这样两个不同的进程 就可以 映射到 相同的物理内存上,从而实现信息的交互。
共享内存是 效率最高的IPC。
编程步骤:
1 获取 key,方式ftok()或头文件定义。
2 使用shmget()函数创建/获取内部ID。
3 使用shmat()挂接共享内存(映射)。
4 可以像正常操作一样使用共享内存。
5 使用shmdt() 脱接共享内存(解除映射)。
6 如果确定已经不再使用,可以使用shmctl()删除共享内存。
key_t ftok(char* pathname,int projectid)
参数pathname是一个真实存在的可访问的路径
projectid是项目编号,低8位有效(1-255)
返回key。
ftok()如果给定路径有效,不会出错。会按照路径和项目ID生成一个key。相同的路径+相同的项目ID生成 相同的key。
int shmget(key_t key,size_t size,int flag)
参数:key 就是第一步返回值,外部的key
size就是共享内存的大小
flag在获取时用0,在新建时用:
IPC_CREAT|0666 (权限)
成功返回共享内存的ID,失败返回-1.
void* shmat(int shmid,0,0) 可以挂接
int shmdt(void* addr)
adrr是shmat()的返回值,首地址(虚拟)。
shmctl() 可以查询、修改和删除共享内存。
查询时,会把共享内存的信息放入第三个参数
修改时,只有用户id、组id和权限可以修改。
删除时,第三个参数给0 即可。
删除共享内存时,挂接数必须为0才能真正删除,否则删除只是做一个删除标记,等挂接数为0时才真正删除。
第三个参数是结构体指针:struct shmid_ds
共享内存虽然速度最快,但当多个进程同时写数据时,会发生互相覆盖,导致数据混乱。
消息队列就可以解决多个进程同时写数据的问题。
消息队列就是 存放消息的队列。队列是线性的数据结构,先入先出(FIFO)。一般情况下,队列有满有空。数据先封入消息中,然后再把消息存入队列。
消息队列的编程步骤:
1 得到外部的key,函数ftok()。
2 用key创建/获取一个队列(消息队列),函数msgget()。
3 把数据/消息 存入队列 或 从队列中取出。
函数 msgsnd()存入/msgrcv()取出
4 如果不再使用消息队列,可以msgctl()删除。
msgget() msgctl()与共享内存的函数相似。
int msgsnd(int msgid,void* msgp,
size_t size,int flag)
参数 msgid就是消息队列的ID
msgp就是消息的首地址,其中消息分为有类型消息和无类型消息,更规范的是 有类型消息。有类型消息就是一个结构:
struct Msg{ //结构的命令可以自定义
long mtype; //消息的类型,必须大于0
... //数据区域,可以任意写
};
将来在接收消息时,可以按照类型有选择的接收消息。
size参数是 数据区域的大小,不包括mtype(有些时候包括了也行)。
flag 就是选项,可以是0 代表阻塞(队列满了等待),也可以是IPC_NOWAIT 代表非阻塞(队列满了直接返回错误)。
成功返回0,失败返回-1.
msgrcv()
int msgrcv(int msgid,
void* msgp,size_t size,long mtype,int flag)
前三个参数和msgsnd()一样。
参数flag和msgsnd()也一样。
参数mtype可以让接收者有选择的接收消息,值可能是:
0 - 接收任意类型的消息(第一个,先入先出)
>0 - 接收 类型为mtype的特定消息
<0 - 接收类型 小于等于 mtype绝对值的消息,从小到大接收。
成功返回接收到的数据大小,失败返回-1.
综合案例需要用到的知识点复习,包括:
文件、创建子进程、信号signal()、消息队列。
今天:
XSI IPC - 信号量集
网络编程 - socket编程
信号量是一个计数器,用于控制访问共享资源的最大并行进程数。信号量集就是信号量的数组。
信号量的工作方式:
设定一个初始计数,每来一个进程计数-1,每完成一个进程计数+1,计数到0不允许进程访问,直到大于0为止。
信号量集的编程步骤:
1 获得key。
2 用semget()获取信号量集的ID。
3 用semctl()设置信号量的初始计数。
4 用semop()进行加1或减1的操作。
5 如果不再使用,用semctl()删除。
其中,semctl()设置初始值的代码:
semctl(semid,index,SETVAL,count)
其中,semid是信号量集的ID,index是信号量在信号量集中的下标,SETVAL是宏,count就是该信号量的初始计数。
int semop(int semid, struct sembuf
semoparray[],size_t nops);
参数semoparray是一个指针,它指向一个信号量操作数组,信号量操作由sembuf结构表示:
struct sembuf{
unsigned short sem_num;//操作信号量的下标
short sem_op; //对信号量操作方式。 -1 和 1
short sem_flg; //0 等待 IPC_NOWAIT不等
};
网络编程(非常重要)
网络常识 - 比如: 协议、IP地址、端口等。
网络编程的步骤和函数
基于TCP/UDP的开发
OSI 七层模型: 物理层、数据链路层、网络层、传输层、会话层、表现层、应用层
协议就是 计算机信息交互时的规范。常见的协议: http协议 - 超文本传输协议
ftp协议 - 文件传输协议
tcp协议 - 传输控制协议 (传输层)
udp协议 - 用户数据报协议(传输层)
ip 协议 - 网络层协议
...
协议簇就是多个协议的集合,协议簇一般都是以核心协议命名。也有非官方的写成协议族。
IP地址就是网络中计算机的唯一标识,本质是一个整数。IP地址早期都是32位的,叫IPV4。后来推出了IPV6(128位)。主流还是IPV4。IP地址有两种描述方式: 点分十进制和十六进制。
点分十进制就是每8位做一个整数(0-255),分4段,中间用点. 隔开。
十六进制就是把32位二进制直接写成 8位十六进制。
计算机更倾向十六进制,而人更习惯点分十进制。
IP地址其实绑定的是网卡,每个网卡在出厂时都有一个唯一的物理地址(MAC地址),IP地址其实是找到网卡的物理地址,找到网卡从而找到计算机。
IP地址分为A/B/C/D 四类。系统预留了
127.0.0.1,做本机的IP地址。
关于网络的一些基本命令:
ipconfig - windows dos命令,查看IP地址
ifconfig - Unix/Linux 查看IP地址。
ping IP地址 - 测试IP地址是否可以访问。
子网掩码 就是用来 判断IP是否同一网段。
IP地址:166.111.160.1
166.111.161.45
子网掩码:255.255.254.0
做位与运算:
166.111.160.1 - 所有偶数 最后一位0,奇数1
255.255.254.0
-------------------
166.111.160.0
166.111.161.45
255.255.254.0
-------------------
166.111.160.0 ---> 结果相同,同一网段
IP地址可以让我们找到计算机,但没有找到对应的进程。在网络中,端口代表计算机内部的一个进程。有IP地址+端口号 就可以网络通信。ip地址+端口号的网络编程就是socket编程。
端口号也是一个整数short,0到65535。其中:
0-1023不要用,系统占用其中很多端口。
48000以后也不要用,不稳定,系统随时可能征用。有些软件会强占一些端口,这些端口不要使用。
Oracle数据库 占1521端口、8080端口等。
常用端口:
Http端口 80
ftp端口 21
telnet端口 23
字节顺序: 整数是4个字节,有些计算机从低位字节到高位字节存储,有些机器从高位字节到低位字节存储。
本机的字节顺序无法确定,但网络的字节顺序是固定的。编程用网络字节顺序传输,到本地以后再网络转本地格式。
Unix/Linux网络编程的实现
有固定套路并且有一些不方便的函数、结构。
socket编程 (插座、套接字)
socket通信包括 一对一 和一对多。
先研究一对一(一对多的模式也一样)
socket编程 早期用来做进程间通信(IPC),现在主体是网络,编程的代码差不多。
1 本地通信(IPC)
1.1 服务器端的编程步骤:
1.1.1 创建一个socket,使用socket()
int socket(int domain,int type,int protocol)
参数:domain叫域,用于选择协议簇
AF_UNIX/AF_LOCAL/AF_FILE : 本地通信IPC
AF_INET : 网络通信
AF_INET6: IPV6的网络通信
其中,AF换成PF效果一样。
type 选择通信的类型(选协议)
SOCK_STREAM : 数据流(TCP协议)
SOCK_DGRAM : 数据报(UDP协议)
protocol 本来应该选择协议,但实际上没什么用,协议已经被前2个参数决定,给0即可。
成功返回 socket描述符,类似文件描述符。失败返回-1.
注:读写函数 可以操作socket描述符。
1.1.2 准备通信地址(IPC是文件,网络是IP/端口)
系统提供了三种通信地址,就是三个结构体。
1 struct sockaddr本身不存数据,做函数的参数。
2 struct sockaddr_un 存本地通信的通信地址
3 struct 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地址
};
1.1.3 绑定socket描述符和通信地址
bind(int sockfd,struct sockaddr* addr,
int length)
length是通信地址的sizeof
1.1.4 通信(read()、write())
1.1.5 关闭socket描述符(close())
2 客户端的编程步骤
和服务器端编程步骤一样,除了第三步把bind换成connect(),但函数的参数不用改变。
bind() 是服务器绑定通信地址,开放端口。
connect()是客户端连接服务器,通信地址要使用服务器的。
本地通信 媒介是 socket文件,类型s。
回顾:
信号量集 - 信号量 控制访问共享资源的最大并行进程数。信号量集 就是信号量的数组。
IPC中,消息队列比较常见,而且综合效果最好。信号量集有不可替代的作用,只要控制并行进程数,必定使用信号量集。
网络编程 - 常识、编程步骤
OSI七层模型、协议、协议簇、IP地址和端口。
编程步骤:
1 socket() 得到socket描述符
2 准备通信地址 struct sockaddr_in
3 服务器是bind(),客户端是connect()
4 read() 或 write() 读写的要求:一读一写
5 colse() 关闭socket描述符。
今天:
在使用网络编程时,IP地址和端口号都需要做一些处理。IP地址需要做点分十进制和十六进制转换,使用函数 inet_addr();端口号需要本机格式和网络格式之间的转换,使用函数htons()。
一对多的编程模型:
TCP协议一对多:
有两种socket描述符,其中一种负责 等待客户端的连接,当有客户端连接时,启动一个新的描述符负责信息交互。
TCP协议是一个基于连接(有连接)的协议,全程保持客户端和服务器的连接。会重发一切的错误数据,因此TCP可以保证数据的完整和有效。缺点就是当客户端超级多的时候,效率非常低。
UDP协议是一个不基于连接(无连接)的协议,发送数据时连接一下,发送完了就断开,而且不考虑是否接收到了。UDP效率比TCP高,UDP不保证数据的有效和完整。QQ/MSN 都是采用UDP协议。
TCP一对多的编程步骤:
服务器端:
1 socket(),得到第一类的socket描述符。
2 准备通信地址 struct sockaddr_in
3 绑定 bind(),开发断开。
4 监听客户端的连接,函数listen()。
5 等待客户端的连接,函数accept(),返回新的socket描述符,用于信息交互。(无客户端连接会阻塞)
6 用第五步返回描述符进行读写操作。
7 close()关闭两个描述符。
客户端的编程步骤与前面的一样。
int listen(int sockfd,int backlog)
设置当多个客户端同时 连接时,需要把多余的客户端存入队列,backlog就是队列的最大长度。
int accept(int sockfd,struct sockaddr*
addr, socklen_t* len)
参数 sockfd就是socket描述符,第一步的返回
addr是一个结构体指针,存客户端的通信地址
len是传入传出参数,先传入addr的长度,再传出接收到的客户端通信地址的真实长度。
返回 新的socket描述符,失败返回 -1.
练习: 改良TCP代码,要求:客户端可以输入(scanf),服务器端回发数据(客户端输入什么,就发送什么)。客户端可以多次输入,输入bye时退出输入的循环。
UDP编程:
TCP和UDP的区分主要在于socket()第二个参数,如果是SOCK_STREAM就是TCP,SOCK_DGRAM就是UDP。
UDP分 发送方和接收方。
UDP发送数据很少使用write(),使用sendto()。
接收数据可以使用两个函数:read()/recvfrom()
区别就是read()函数不知道数据的来源,而recvfrom()可以获取数据的发送者信息。
ssize_t sendto(int sockfd,void* data,size_t
length,int flags, struct sockaddr* addr,
socklen_t size)
参数:前三个 参数与write()一样,flags一般给0即可,addr就是第二步的通信地址的指针,size就是sizeof(addr)。
成功返回发送的字节数,失败返回-1.
注:sendto()好像write()和connect()的合体。
ssize_t recvfrom(int sockfd,void* data,size_t
length,int flags, struct sockaddr* addr,
socklen_t* size)
参数:前三个和read()一样,flags给0即可,addr是用于存储发送者通信地址的指针,size是一个传入传出参数,把addr的sizeof传入,再传出真正获取到的通信地址的大小。(后两个参数和accept()一样)
成功返回接收到的字节数,失败返回 -1.
注:recvfrom像 read() 和 accept()的合体。
UDP练习:
写一个基于UDP的时间服务器。
时间服务器提供的功能就是: 当客户端发送请求时,发回当前的系统时间。时间服务器要写成死循环,用信号退出。
提示:系统时间找 time() 获得秒差,函数localtime()负责把秒差转成 年月日小时分秒的格式,返回给客户端。localtime()返回时间的结构体指针 struct tm,具体成员 在localtime的手册中可以看到。
网络编程:
基于TCP的编程步骤:
Server端:
1 socket()
2 准备通信地址 struct sockaddr_in
3 绑定 bind()
4 listen()
5 accept(),返回一个用于交互的新的描述符
6 读写 read() write()
7 关闭close()
Client端:
1 socket()
2 准备通信地址 struct sockaddr_in
3 连接 connect()
4 读写 read() write()
5 关闭close()
基于UDP的编程步骤:
接收方:
1 socket()
2 准备通信地址,struct sockaddr_in
3 绑定 bind()
4 发送或接收 sendto() recvfrom() read()
5 关闭close()
发送方:
1 socket()
2 准备通信地址,struct sockaddr_in
3 发送或接收 sendto() recvfrom() read()
4 关闭close()
今天:
线程(thread) - 做应用,有网络必有线程。
主流的操作系统都是支持多进程的,每个进程的内部可以启动多线程完成代码的并行;每个线程的内部可以无限启动多线程。
线程是轻量级的代码并行,不需要额外创建过多的内存空间,而是共享所在进程的内存空间。线程只需要额外建一个独立的栈即可。
多线程之间互相独立,又互相影响。
多线程可以大幅提升代码的效率。
程序的运行必须拥有CPU和内存,内存可分, CPU不可分,如何实现并行。大多数的操作系统都是采用CPU时间片实现CPU的在多线程之间的轮换。CPU时间片是极短的一段CPU的执行时间,拥有CPU时间片的线程有机会运行。
比如:人的感官是需要时间的,比如视觉0.1秒。就是100毫秒。假定CPU时间片是1毫秒,有4个线程。每个线程先分一个时间片,也就是1毫秒的CPU运行时间。每个线程只能运行1毫秒,时间片的运行时间到了以后就只能看其他线程运行,直到所有线程的时间片都运行完毕,再重新分配。
针对时间点的并行是不存在的,针对时间段的并行就是我们通常说的代码并行。
每个进程都有一个主线程,就是main()函数,主线程结束,进程也结束,同时导致所有线程都结束。
线程的编程:
Unix/Linux的线程相关函数都在pthread.h中,代码都在libpthread.so中。线程相关的函数/结构都以pthread_ 开头。比如创建线程函数:
pthread_create();
int pthread_create(pthread_t* id,
pthread_attr_t* attr, void* (*fa)(void*),
void* arg)
pthread_create()是一个四针函数,参数id就是用于存储线程ID的;attr是线程的属性,一般给0即可(默认属性); fa是一个函数指针,写线程执行的代码;arg是传给fa的参数。fa+arg指定了线程要执行的代码。
返回值: 成功返回0,失败返回错误码,想看错误信息需要用strerror()做转换。
每个线程启动以后,只能执行一个函数,主线程执行的是main(),其他线程执行自定义的一个函数。这个函数以并行的方式运行。
线程之间的代码乱序执行,每个线程的内部代码都是顺序执行。每个线程都会返回自己的错误码,而不是使用errno。
pthread_join()函数可以让一个线程等待另外一个线程的结束,并取得线程的返回值。
如果在线程a中调用了pthread_join(b,0),线程a就会等待线程b的结束,等线程b结束以后a才能继续运行。
线程传参时,一定要注意保证地址的有效性,尤其是堆内存。支持直接传递int。
关于函数的返回:
1 能返回局部变量,但不能返回指向局部变量的指针。
2 static的局部变量的地址可以返回(全局区)。
3 数组理论上可以做返回值,但返回值类型不能写数组。最好用指针。
int[] get() 错.
void fa(int* pi){ *pi = 200;}
int main(){
int x;
fa(&x); -> pi = &x ->*pi = x = 200;
}
void fa(int** pi){ *pi = 地址1;}
int main(){
int* px;
fa(&px); ->pi = &px ->*pi = px = 地址1;
}
线程的状态:
线程应该处于以下两种状态:
1 分离状态
就是线程一旦结束,不用管其他线程,直接回收资源。函数pthread_detach()设置线程分离状态。
2 join状态
如果线程用pthread_join(),就处于join()状态,就是线程结束时暂不回收资源,到pthread_join()函数结束时再回收资源。
注:没有分离也没有join()的线程资源回收是没有保障的。
分离状态的线程再调用pthread_join()没有效果
线程的退出
正常退出:
在线程的函数中执行了return语句。
执行了pthread_exit(void*)函数
非正常退出:
自身出现错误
被其他线程终止/取消
exit()和pthread_exit(void*)的区别?
exit() 是结束进程,所有线程全结束
pthread_exit()是结束线程,其他线程继续运行
参数void* 和 return 一样,都是 用于返回值。
取消线程的函数: pthread_cancel()
多线程之间是共享进程的资源,因此有可能出现共享数据的冲突,解决方案就是把并行访问改为串行访问,这种技术叫线程同步。线程同步的技术包括: 互斥量、信号量、条件变量。
互斥量又叫互斥锁,是线程在设计时官方的同步技术,编程步骤如下:
1 声明互斥量
pthread_mutex_t lock; //变量名不一定叫lock
2 初始化互斥量
pthread_mutex_init(&lock); 或在声明的同时赋值: pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
3 加锁/上锁 pthread_mutex_lock(&lock);
4 执行共享数据的访问代码
5 解锁 pthread_mutex_unlock(&lock);
6 释放锁的资源
pthread_mutex_destroy(&lock);
信号量是一个计数器,用于控制访问共享资源的最大的并行 进程/线程的数量。
信号量的工作原理:先设置最大值最初始计数,每上来一个计数减1,每退出一个就加1,到0就不允许新进程/线程访问,除非计数又回到大于0。
信号量不属于线程的范围,不在pthread.h中,只是一个线程计数辅助。头文件 semaphore.h
信号量如果初始计数为1,效果等同于互斥量。
信号量的编程步骤:
1 声明信号量 sem_t sem;
2 初始化信号量的原始计数 sem_init()
sem_init(&sem,0,count)
第一个参数就是信号量的地址
第二个参数必须是0,0代表线程的计数,非0代表进程的计数(Linux系统没有提供进程计数功能)。
第三个参数就是 计数的初始值(最大计数)。
3 计数减1 sem_wait(&sem);
4 正常使用
5 计数加1 sem_post(&sem);
6 释放信号量资源 sem_destroy(&sem);
使用线程同步技术,小心避免死锁。
pthread_mutex_t lock1,lock2;
线程a:
lock(&lock1);
...
lock(&lock2); //等待线程b unlock(&lock2)
...
unlock(&lock2);
unlock(&lock1);
线程b:
lock(&lock2);
...
lock(&lock1);//等待线程a unlock(&lock1)
...
unlock(&lock1);
unlock(&lock2);
结果就是看起来都问题,但执行 a和b互相锁定,死锁。