一、Linux系统概述
不加引号可理解为宏,直接替换,单引号中特殊字符会被解释为普通字符,双引号中$,,'还是特殊。反引号可嵌套但里面的要加\。帮助命令man ls或ls --help。netstat -a显示所有连接中的socket。
二、C语言编程基础
逗号运算符功能是把两个表达式连接起来组成一个表达式,分别求值并把表达式2的值作为整个逗号表达式的值 。char* str = "AA BB"; char string[] = "AA BB"; 字符串指针是个变量可以改变指向不同的位置但不能改其值,但可以改变字符串数组的值。getchar(),putchar()。printf里的%d与%i效果一样。%g以%f%e中较短的输出宽度输出。scanf对于%f可指定数据宽度但不能指定精度如%10f正确便%10.2f错误,对long必须是%ld,对double必须是%lf或%le。而%f/s/d中的表使对应的输入数据不赋值给相应的变量。外部变量(即全局变量)是定义在函数外部的,如不在文件开头定义则作用域为定义位置到结尾,可在使用前用extern进行声明就可以正常使用。只有局部自动变量和形参可以作为寄存器变量,且不能使用&求寄存器变量的地址。puts,gets(fgets)(不是以空格为结束而是以换行为结束标志)。strlen不含\0的实际长度。 指向函数的指针:返回值类型 (指针变量名) (),()优先级比高,不加()指针变量名首先会与后面的()结合。 指向结构体的指针使用时如(ptr_struct).name中()不可省略,因为.的优先级高于。不能引用共用体变量,只能引用其成员。共用体中起作用的成员是最后一次存放的成员,在存入一个新成员后,原有成员就失去作用,共用体变量的地址和它的各成员的地址都是同一地址,不能在定义共用体变量时对它时行初始化,不能对共用体变量名赋值,也不能企图引用变量名来得到一个值。更改值时直接对其成员赋值即可。 建立链表typedef struct node{ int data; struct node* link;}NODE; NODE* head; 位域:为节省空间,把一个字节中的二进制位划分为几个不同的区域,并说明每个区域的位置,每个域有一个域名,允许在程序中按域名进行操作。struct a{char b : 3; char c : 3; char d :2;};一个位域必须存储在同一个字节中,不能跨两个字节,使用时和结构体方式一样。 预处理可提高编程效率,它们不是C语言本身的组成部分,不能直接编译,带#的表预处理命令。#error error-message 强制编译停止,并输出错误信息,控制编译,用于判断编译是按设定的流程进行的。#line number "filename"改变LINE和FILE的内容。
三、vi与Emacs编辑器
vi即Visual Interface。输入":"、"/"、"?"进入底行模式。插入模式下任何输入都作为文件内容,不能当作底行命令,反之亦然,因此两者切换要经过命令行模式。命令模式下:移动光标:^,,dd,ndd,nd+上/下方向键。其他命令:r,R多次,u,U,.(重复上次命令),ZZ,%。插入模式下:i,I,a,A,o,O。底行模式也叫最后行模式,用于搜寻、替代、保存、退出,q,q!,x,x!,w,w!,wq,w filename,w! filename,r filename,bn, bp,s/pattern1/pattern2/g,%s/pattern1/pattern2/g,g/pattern1/s//pattern2, num1,num2s/pattern1/pattern2/g,/向后查找,?前,N继续查找下一个。在替换功能中说明:%指所有行,g表对行中每个匹配都替换,否则只替换行的第一个匹配,s表示其后是一个替换命令。 Emacs即Editor Macros编辑器宏的缩写。退出是Ctrl+X,Ctrl+C。Emacs默认工作目录是当前用户的主目录。当打开或编辑一个文件时,Emacs将自动创建一个缓冲区,一个文件对应一个缓冲区,用户在窗口进行编辑操作,输入的字符将会被暂存在缓冲区,当用户执行保存操作时,Emacs会自动将缓冲区中的内容保存到当前打开文件中,Emacs允许一次打开多个文件,即使用多个缓冲区。文件操作:C-x,C-f,C-x d,C-x i,Cx
四、gcc编译器与gdb调试器
gcc(GNU Compiler Collection),预处理、编译、汇编、链接。后缀名.c,.a(由目标文件构成的档案库文件),.C(.cc,.cxx)C++源文件,.h,.i经预处理的C源文件,.ii,,.o,s汇编语言源代码文件,.S经预处理的汇编语言源代码文件。编译选项:-o,-c,-S,-E,-g,-v,-I dir(在头文件的搜索路径列表中添加dir目录) ,-L dir, -static,-library,-O,-O2,-O3,-Wall(显示所有警告),-Werror(警告当作错误),-w,-pedantic。先调用预处理程序cpp(#include,#ifdef,#definet等,gcc -E h.c -o h.i),再调用ccl、as编译出目标代码(gcc -S h.i -o h.s进行编译生成汇编语言,gcc -c h.s -o h.o进行汇编把前面生成的文件汇编成具体CPU上的目标代码模块,生成机器语言.o),最后调用ld生成可执行程序(gcc h.o -o hello)。优化:-O即-O1,一般包括线程跳转和延迟退栈,-O2除O1外还包括调整处理器指令调度等,-O3在O2上还包括循环展开等。time ./h可得到程序运行大致时间,time的结果中,real指进程总时间,它和系统负载有关(包括进程调度、切换的时间),user指进程中用户指令的时间,sys指进程中内核代用户指令的时间,user和sys的和被称为CPU时间。 程序开发的时候,优化会增加编译时间先不优化,发布的时候再优化,内存小的时候如嵌入式设备不要优化,会增加代码体积。函数库实质上是一些头文件和库文件(.so,.a)的集合。Linux下大多函数的头文件默认路径是/usr/include,库文件/usr/lib,添加头文件路径:gcc foo.c -I /home/chiguo/include -o foo。对于库文件,如有个libfoo.so,gcc foo.c -L /home/chiguo/lib -lfoo -o foo,其中-lfoo,中-l指示gcc链接libfoo.so。Linux下库命令应以lib开头。动态库是在运行时动态加载的,静态库是在编译时静态加载的。默认gcc优先使用动态库,不存在时,才考虑用静态,也可-static强制使用静态,如目录下有libfoo.so,libfoo.a,可用gcc foo.c -L /home/chiguo/lib -static -lfoo -o foo。gcc f1.c f2.c f3.c -o f相当于先对每个生成.o,再链接,即最后gcc f1.o f2.o f3.o -o f。使用管道,可避免生成临时文件,它实质是进程间的通信方式,可用来同时连接两个程序(进程)其中一个输出直接作为另一个的输入,避免生成临时文件,但却要消耗更多的内存,大型程序效果很明显,gcc -pipe -Wall foo.c -o foo。 使用-g,-ggdb增加调试信息,也分级-g1,-g2(默认),-g3,g1只包含回溯跟踪、堆栈等,g2在g1上增加包括行号、局部或外部变量信息等,g3在g2上增加源代码中定义的宏。GDB,GNU Debugger。file,run,kill,step,next,break,print(还可对变量赋值),display,list,quit,watch,backtrace,frame n(定位到发生错误代码段,结合bt使用),examine,jump,singal,return,call,make,shell。设置断点:break(b或list,jump)后接 fun,line,+-offset,filename:linenum(func),address, ...if
五、make的使用和Makefile的编写
make执行后有3个退出码,成功,出错返回1,如果用了-q选项且make使得一些目标不需要更新,返回2。名字为Makfile或makefile或makefile文件夹下文件。规则:target ...:prerequisites ... command ...... 。make [options] [target] ...其中选项可是-f filename,-C dirname,-e,-k,-n,-p,-r,-s,-S,-t,-I,-V。一条规则或命令在一行中写不完时用""表换行 。清空中间文件make clean。目标名称惯例:all,clean,distclean,install。命令要以tab开头(或与prerequisites在同行,用;分隔)。make会比较targets文件和prerequisites文件的修改日期,如果prerequisites更新或targets不存在,make会执行后续定义的命令,如自定义标签但不定义依赖性,则不会自己执行,可make命令后显示指出此标签来执行,如用于clean中间文件,或打包、备份等。makefile里的第一条规则中的第一个目录会作为最终目标。Makefile主要包含5方面内容:显示规则、隐式规则、变量定义(类似于宏)、文件指示(一个Makefile包含另一个,类似于include,二个根据条件指定Makefile中有效部分,类似于#ifdef,三个是定义一个多行的命令)和注释#。通配符*,?,[]。Makefile中变量其实是C++中的宏,不会展开,如果想展开要用objects := @,它表规则中所有的目标的集合,如bigoutput littleoutput: text.g generate text.g -@) >; ,最好用()或{}引起来,使用真实的<"属于规则型变量,这种变量的值依赖于规则的目标和依赖目标的定义,也可为某个目标设置局部变量,这种变量称为目标变量,它的作用范围只在这条规则及连带规则中,语法:
循环函数:@"放入到表达式中,因为自动化变量是在运行时才有的。(call
六、文件I/O操作
文件结构是文件存放在磁盘等存储设备中组织方法。管道文件主要用于在进程间传递数据,把管道作为文件进行处理可分为匿名管道和命令管道。套接口文件分三种,流式TCP,数据报UDP和原始SOCK_RAW。基于文件描述符是不带缓冲的IO,UNIX shell将文件描述符0与进程的标准输入相结合,1与输出相结合,2与标准出错相结合,使用STDIN_FIFLNO,STDOUT_FILENO,STDERR_FILENO,文件描述符是由无符号整数表示的句柄,进程使用它来标识打开的文件,它与包括相关信息(如打开模式等)的文件对象相关联,这些信息被做文件的上下文。重定向输入:command 1> filename是把标准输出重定向到一个文件中(1和>中不能有空格,此方式同command > filename)直接> file相当于创建空文件。 command << delimiter从标准输入中读入,直到遇到delimiter分界符。重定向标准错误出错:command 2>(>>) filename。command > filnename 2> &1把标准输出和标准出错一起重定向到filename中,如在命令行下输入xxx会把bash:xxx:command not found就是标准出错,可xxx 2 > tmp。 函数:open(open返回的文件描述符一定是最小的末用描述符数字,如先关闭标准输入1,再打开一个文件则其描述符一定是1),creat(建议直接用open),close(当一个进程终止时,它所有的打开文件都由内核自动关闭,不建议的也可用这种方式不显示的close文件),当前文件位移量:off_t lseek (int fd, off_t offset, int whence);其中l表long是长整型。文件空洞(seek)不要求占用内存空间(但是占长度),read,write。od -c 文件名 以字符方式打印文件内容。 改变权限:chmod,fchmod函数。改变所有者(可同时改组):chown,fchown,lchown(只针对链接文件)函数。重命名函数:rename。修改文件长度:stat结果的st_size,对符号链接其文件长度是文件名中的实际字节数。截断函数:truncate,ftruncate。Linux系统中所有文件都有一个与之对应的索引节点,包含了文件的相关信息,并保存在stat结构中,stat,fstat,lstat。复制一个现存的文件描述符:dup,dup2。改变已打开文件的性质:fcnt1。写入文件:sync将所有修改过的块的缓存排入写队列然后返回,不等待实际I/O操作结束,系统进程(通常称为updae)一般每隔30s调用一次sync,fsync只引用单个文件,且等待I/O结束返回。 目录文件的操作:mkdir函数,rmdir函数,opendir(返回DIR类型它是用于指向目录文件的结构指针),closedir,readdir,当前目录:(每个进程都有一个当前目录)chdir,fchdir,getcwd函数。 链接文件:硬链接link函数,删除链接unlink(也可用remove,对文件其功能与unlink相同,对目录功能同mkdir)。硬链接要求链接和文件位于同一文件系统中,且只有超级用户能创建。它直接指向文件的i节点。symlink软链接,readlink是读取链接本身,而不是其指向的文件。管道文件int pipe(int filedes[2]);。
七、基于流的I/O操作
当使用流I/O时,stdin,stdout,stderr也会自动打开。有三种类型缓存,全、行与无缓存(标准出错用的这种)。设置缓存的属性(包括缓冲区的类型、大小):setbuf,setbuffer,setlinebuf,setvbuf(最好用此),最好刚开始还未使用时进行设置。缓存的冲洗:将I/O操作写入缓存中的内容清空(可丢掉fpurge或保存到文件flush)。流的打开:fopen,freopen,fdopen(取一个现在的文件描述符并使一个标准的I/O流与该描述符相结合)。Linux系统不区分二进制文件和文本文件,它们都是普通文件。未调用fclose可能会导致数据停在缓冲区,而没有保存到文件中。fprintf向流中写。一旦打开了流,可在4种不同类型的I/O中进行选择来对其读写:基于字符(getc,fgetc,getchar,putc,fputc,putchar)、行(fgets,gets,fputs,puts)、直接I/O(fread,fwrite)用这种方式可以直接读取一个结构如一个struct,格式化I/O(printf,fprintf,sprintf,snprintf,scanf,fscanf,sscanf)。判断读入出错还是结束:ferror,feof。
八、进程控制
Linux系统中所有进程除了初始化进程外都有一个父进程,新进程不是被创建,而是被复制或从以前的进程复制而来。Linux系统中所有进程都是由进程号为1的init进程衍生而来的,进程树。一个正在执行的进程称为一个作业,而作业可以包含一个或多个进程,尤其是使用了管道和重定向命令的时候。在Linux中每个进程在创建时都会被分配一个数据结构,称为进程控制块(PCB)。进程ID 0是调度进程,常被称为交换进程(swapper),它是内核的一部分(系统进程),init的ID为1,它是普通的用户进程,但它以超级用户特权运行。多个进程合成进程组,多个进程组合成一个会话,getpid()。创建进程pid_t fork();成功父进程返回子进程ID,子进程返回0,出错返回-1。子进程从父进程得到数据段和堆栈段的拷贝,这些需要分配新的内存。现在很多是采用写时复制(Copy-On-Write,COW)技术,运行后两进程独立,同一个变量不相通(不是共享数据段)。vfork不需要完全拷贝父进程的数据段(如果先copy会大量拷贝数据子进行exec时会再清除这些数据,因而先不copy,真正用时再copy),在子进程没有调用exec或exit之前,子、父进程共享数据段,且子进程先运行,父进程挂起,直到子进程调用了exec或exit之后父子进程的执行次序才不再有限制,vfork创建出来的是一个线程(没有独立的内存资源),共享数据段,一个改变会影响另一个。 Linux使用exec来执行新的程序,以新的子进程来完全代替原有的进程。execl,execlp,execle,execv,execvp,execve,只有execve才是真正意义上的系统调用,其他都是在此基础上经过包装的库函数,exec函数族的作用是根据指定的文件名找到可执行文件,并用它来取代调用进程的内容。exec执行成功不会返回,只有失败了才返回-1。perror用来将上一个函数发生错误的原因输出到标准错误,原型是void perror(const char* s),参数s所指字符串先打印后面再加上错误原因。 退出程序可以有返回值用wait接收并处理。_exit立即进入内核,exit则先执行一些清除处理(包括调用执行各终止处理程序,关闭所有标准I/O流,把缓冲区中的内容写回文件等如果此时直接调用_exit会丢失缓冲区中的内容,如调用printf没有\n换行及fopen,fread,fwrite等),然后进入内核。进程调用exit后,并非马上就消失掉,而是留下一个称为僵尸进程的数据结构,处理方法是调用wait和waitpid成功返回子进程ID,失败返回-1。僵尸进程放弃了几乎所有内存空间,没有任何可执行代码,也不能被调度仅在进程列表中保留一个位置,记载该进程的退出状态等信息供其他进程收集,除此外不再占用任何内存空间。进程一旦调用wait就立即阻塞自己,并由wait分析是否当前进程的某个子进程已经退出,如果找到这样一个僵尸子进程,会将它彻底销毁并返回,如果没找到wait就一直阻塞,直到有一个出现为止,如果出现wait就会收集这个子进程的信息并把它彻底销毁后返回,如果只想把僵尸进程消灭掉而对进程如何死掉毫不在意可直接pid=wait(NULL),而不用pid=wait(int* status)。通过WIFEXITED(status)(用来指示子进程是否为正常退出),WEXITSTATUS(status)(如果WIFEXITED返回非0用此来提取子进程的返回值)两个宏可获取子进程退出时的返回值。还有wait3,wait4功能同wait但多个rusage是一个结构指针,可获取进行及其子进程所占用resuorces的情况。 进程用户ID,用户组ID,实际、有效、保存的设置用户(组)ID。getuid,getgid(前两个是实际),gettuid,getegid,setuid,setgid,seteuid,setegid。只有超级用户进程可以更改实际用户ID,setreuid,setregid用于交换实际及有效用户(组)ID。system执行系统命令也是调用程序创建进程来实现的(通过fork,exec,waitpid),system(const char* cmdstring)。获取进程组ID,pid_t getpgrp();每个进程组有一个组长进程leader,其标识是进程组ID等于其进程ID,进程组长可创建一个进程组成可创建该组中的进程,进程组中最后一个进程可终止或加入到另一进程组中,setpgid可加入另一组或创建新组。会话期是一个或多个进程组的集合。建立对话期:setsid,获得与设置前台进程组ID:tcgetpgrp,tcsetpgrp。
九、信号
信号全称为软中断信号,它是一种进程间通信的方法,应用于异步事件的处理。处理信号的方式可类似中断的处理指定处理函数,或忽略,或对该信号的处理保留系统的默认值,这种默认操作大多数是使进程终止。在进程表的表项中有一个软中断信号域,该域中每一位对应一个信号当有信号发来时对应位置位,进程对不同信号可同时保留但对同一信号并不知道在处理前来过多少个。当进程调用abort函数时会产生SIGABRT信号。用kill -l查看,131为传统UNIX支持的信号是不可靠(非实时)的,3263是后来扩充的,是可靠信号(实时信号)区别在于前者不支持排队可能会造成信号丢失。 SIGTERM是程序结束信号,与SIGKILL不同的是该信号可被阻塞和处理,通常用来要求程序自己正常退出,shell命令kill默认产生这个信号如果终止不了才尝试SIGKILL。SIGCHLD是子进程结束时父进程收到的信号,若父进程没有处理此信号也没等待子进程,子进程终止后还会在内核进程表中占有表项即僵尸进程,可父进程先终止这时子进程的终止自动由init进程来接管。SIGSTOP暂停执行,同SIGKILL一样不能被阻塞处理或忽略(为了使系统管理员在任何时候结束或停止某一特定进程的执行)。SIGSTP可被处理和忽略(即Ctrl+Z发出的信号)。 内核给一个进程发送软中断信号的方法是在进程所在的进程表项的信号设置对应于该信号的位(内核通过在进程的struct tast_struct结构中的信号域中设置相应的位来实现向一个进程发送信号)。若信号发送给一个正在睡眠的进程要看该进程进入睡眠的优先级,若进程睡眠在可被中断的优先级上则唤醒否则设置进程表中信号域相应的位而不唤醒进程,这一点比较重要,因为进程检查是否收到信号的时机是一个进程在即将从内核态返回到用户态时或在一个进程要进入或离开一个适应的低调度优先级睡眠状态时。内核处理一个进程收到信号的时机是在一个进程从内核态返回用户态时,所以一个进程在内核态运行时软中断信号并不立即起作用,进程只有处理完信号才会返回用户态,进程在用户态不会有未处理完的信号。内核处理一个进程收到的软中断信号是在该进程的上下文中,因此进程必须处于运行状态。内核执行用户定义的函数方法是内核在用户栈上创建一个新的层,该层中将返回地址的值设置成用户定义的处理函数的地址这样进程从内核返回弹出栈顶时就返回到用户定义的函数处,从函数返回再弹出栈顶时才返回原先进入内核的地方,这样做的原因是用户定义的处理函数不能且不允许在内核态下执行(如果用户定义的函数在内核态运行的话用户就可以获得任何权限)。如果要捕捉的信号发生于进程正在一个系统调用中时,且该进程睡眠在可中断的优先级上,这时该信号引起进程做一次longjmp调用,跳出睡眠状态,返回用户态并执行信号处理例程。有些地方要求进程在检查收到的信号后从原来系统调用中直接返回而不是等到该调用完成,这种进程突然改变其上下文的情况就是使用setjmp和longjmp的结果。setjmp将保存的上下文存入用户区并继续在旧的上下文中执行。进程执行一个系统调用,当因为资源或其他原因要去睡眠时内核为进程做了一次setjmp,如果睡眠中的信号被唤醒进程不能再进入睡眠时,内核为进程调用longjmp,该操作是内核为进程将原先setjmp调用保存在进程用户区的上下文恢复成现在的上下文,这样就使得进程可以恢复等待资源前的状态,而且内核为setjmp返回1,使得进程知道该次系统调用失败。通过调用signal函数来注册某个特定信号的处理程序,原型为void (signal (int signum, void (handler) (int))) (int);其中signum是SIG开头的那些,handle是SIG_IGN或SIG_DFL或接收到此信号后要调用的函数地址。此函数两个参数一个是整型signum一个是函数指针。如signal(SIGINT, SIG_IGN),或void sign_handler(int sign_num){printf("Capture signal number:%d\n", sig_num);然后signal(SIG_INT, sign_handler);(Ctrl+C产生SIGINT)。信号是进程间通信机制中唯一的异步通信机制。int sigaction(int signum, const struct sigaction* act, struct sigaction* oldact);检查、修改与指定信号相关联的处理动作,完全可替代signal函数。处理函数:void handler(int sign_num, siginfo_t* p_sign_info, void* p_reserved); 一个用户进程常常需要对多个信号进行处理,为方便同时处理引入信号集(signal set)的概念,类型sigset_t。sigemptyset,sigfillset,sigaddset,sigdelset,sigismember。发送信号函数:kill,raise,sigqueue,alarm,setitimer,abort。(1)、其中int kill(int pid_t pid, int signum)向进程(组)发送信号,signum为0时(即空信号)实际上并不发送信号只检查如目标进程是否存在、当前进程是否有向目标发送信号的权限等(非root的进程只能向属于同一个session或同一个用户的进程发送信号);WIFEXITED(status)若此值非0表明进程正常结束,可通过WEXITSTASTUS(status)获取进程退出状态(exit时参数)。WIFSIGNALED(status)为非0表进程异常终止可用WTERMSIG(status)获取信号编号。(2)、int raise(int signum)向自身的进程发送一个SIGABRT信号,使进程非正常结束。(3)、int sigqueue(pid_t pid, int signum, const union signal val);主要针对实时信号提出的(也支持前32种)支持信号带有参数,通常与sigaction配合使用,功能同kill,也可signum=0但不能发信号给进程组,在调用sigqueue时sigval_t指定的信息会拷贝到3参数信号处理函数(3参数信号处理函数指信号处理函数由sigaction安装并设定sa_sigaction指针。sigqueue发送非实时信号时仍不支持排队即在信号处理函数执行过程中到来的所有相同信号都被合并为一个信号。 (4)、alarm函数专门为SIGALRM信号而设使系统在一定时间之后发送信号,原型unsigned int alarm(unsigned int seconds);如果调用alarm之前进程中已经设置了闹钟时间则返回上一个闹钟时间的剩余时间否则返回0。调用alarm后任何以前的alarm调用都将无效,如果参数seconds为0,那么进程内将不再包含任何闹钟时间。 (5)、setitimer功能类似alarm但功能更强大,int setitmer(int witch, const stuct itimerval* value, struct itimerval* oldvalue);witch是定时器类型,可设定绝对时间(系统时间)、程序执行时间(只有在用户模式下才可跟踪时间)和从用户进程开始后开始计时。 (6)、abort。 有时不希望进程接收到信号时立刻中断也不希望信号被忽略,可用阻塞信号方法实现,有sigprocmask,sigsuspend。其中sigprocmask可用于检测或更改进程的信号掩码,信号掩码是由被阻塞的发送给当前进程信号组成的信号集。函数sigaction中设置被阻塞信号集合只是针对要处理的信号,而sigpromask是全程阻塞,被阻塞的信号将不能再被信号处理函数捕捉直到重新设置阻塞信号集合,原型如下:int sigprocmask(int how, const sigset_t* set, sigset_t* oldset);how可取SIG_BLOCK,SIG_UNBLOCK,SIG_SETMASK。sigsuspend函数使进程挂起int sigsuspend(sigset_t* sigmask); Linux下有两个睡眠函数unsigned int sleep(unsigned int seconds);和void usleep(unsigned long usec);其实sleep内部是用信号机制进行处理的,用到alarm和int pause(void);其中pause将自身进程挂起,直到有信号才从pause返回。 Linux为每个进程维护3个计时器,分别是真实计时器(程序运行的实际时间)、虚拟计时器(运行在用户态的时间,可认为实际时间减系统调用和程序睡眠时间)和实用计时器(程序处于用户态和内核态所消耗的时间之和)。设定好计时器后该计时器就会定时向进程发送时钟信号分别为SIGALRM,SIGVTALRM,SIGPROF,用到的函数有int getitime(int which, struct itimerval* value)和int setitimer(int which, const struct itimerval* value, struct itimer val* ovalue);
十、进程间通信
进程间通信(InterProcess Communication,IPC)。进程的用户空间是互相独立的一般不能互相访问,但共享内存区是可以都访问到的,也可通过外设如普通文件等但效率太低。进程间通信的方法有管道、消息队列、信号量、共享内存和套接口等。消息队列、信号量、共享内存通称为系统(POSIX和System V系统)IPC。套接口用于远程通信,其余用于本地进程间通信。管道可用于具有亲缘关系进程间的通信,命名管道还可用于无亲缘关系进程间的通信。消息队列包括POSIX消息队列System V消息队列。有足够权限的进程可向队列中添加消息,被赋予读权限的进程则可读走队列中的消息,它克服了信号量承载信息量少,管道只能承载无格式字节流及缓冲区大小受限等缺点。共享内存是最快的IPC形式,是针对其他通信机制运行效率较低而设计的,往往与其他通信机制如信号量结合使用来达到同步及互斥。信号量主要作为进程或线程间同步的手段。进程间通信包括程序运行的实时数据也包括对方的代码段。
(10.1.1)、 管道(Pipe)是在两个进程间实现一个数据流通的通道,简单易用,它实现数据以一种数据流的方式在进程间流动。管道上的数据读出后就没有数据了,和文件不同。匿名管道没有实名并不在文件系统中可以看到,它只是进程的一种资源,会随进程的结束而被系统清除。创建一个管道会生成两个文件描述符。管道一般是半双工的,需要双方通信时需要建立两个管道,只能用于父子进程或兄弟进程之间(亲缘关系的进程)。管道构成一种文件文件系统只存在于内存中。管道的缓冲区是有限的(只存在于内存中,在创建时为缓冲区分配一个页面大小),且所传送的是无格式字节流,就需要双方事先约定好如多少字节算一个消息。int pipe(int fd[2])。其中fd[0]是读出端,df[1]是写入端的文件描述符,关闭用close(fd[0]),close(fd[1])。一般的I/O函数都可以用于管道,如close,read,write等。当对于一个读端已经关闭的管道进行写操作时会产生信号SIGPIPE。管道的读取规则是如果写端不存在则认为已经读到了数据的末尾,读函数返回读出字节数0,当管道写端存在时,如果请求字节数目大于PIPE_BUF时管道中现在的字节数,此值不同内核不同可在/usr/include/linux/limits.h中查看。当管道已经满时写操作将阻塞。只有当管道在读端存在时,向管道中写入数据才有意义否则写入数据的进程将收到内核传来的SIGPIPE信号,可以处理该信号也可忽略(默认动作则是应用程序终止)。如要建立一个你进程到子进程的数据通道可先调用pipe紧接着调用fork,由于子进程自动继承父进程的数据段,则父子进程同时拥有管道的操作权。
(10.1.2)命名管道:也称FIFO是一种文件类型在程序中可通过查看文件stat结构中st_mode的值来判断该文件是否为FIFO。它不同于管道之处在于它提供一个路径名与之关联,以FIFO的文件形式存在于文件系统中,这样只要能访问FIFO就要通信。它是一种特殊的文件存放于文件系统中而不像管道存放于内存(使用完毕后消失),除非对它删除否则不会消息。FIFO不支持lseek等文件定位操作,在shell中可用mkfifo建立其中参数-m mode指出八进制模式,函数:int mkfifo(const char* pathname, mode_t mode);由于FIFO文件的特性,它被隐性地规定不具有执行权限。FIFO比管道多了个open,因为它一种文件类型,而管道不是。要删除命名管道用unlink。
(10.2)消息队列:是一种以链表式结构组织的一组数组,存放在内核中,由各进程通过消息队列标识符来引用的一种数据传送方式,也是由内核来维护。消息队列具有特定的格式及优先级,消息队列是随内核持续(kernel-persistent)的,不是随进程(process-persisten)。随进程持续:IPC一直存在直至打开IPC对象的最后一个进程关闭该对象为止,如管道和命名管道,随内核持续:IPC一直持续到内核重新自举或显示删除该对象为止,如消息队列、信号量、共享内存,随文件系统持续:IPC一直持续到显示删除该对象为止。目前主要有两大类型的消息队列:POSIX及System V。System V目前被大量使用,考虑到移植性应尽量使用POSIX。系统中记录消息队列的数据结构(struct ipc_ids msg_ids)位于内核(因消息队列随内核持续)中,系统中所有消息队列都可在结构msg_ids中找到访问入口。每个消息队列都有一个队列头,用struct msg_queue来描述。队列头中包含了该消息队列的大量信息,包括消息队列的键值、用户ID、组ID、消息队列中的消息数目等,甚至记录了最近对消息队列读写进程的ID。struct msqid_ds用来设置或返回消息队列的消息,存在于用户空间。全局数组结构struct ipc_ids msg_ids可访问每个消息队列头的第一个成员struct ipc_perm,而每个struct ipc_perm能够与具体消息队列对应起来是因为该结构中,有一个key_t类型成员key,而key则唯一确定一个消息队列。
消息队列的ID是由在系统范围内唯一的键值生成的,而键值可以看做对应系统内的一条路径。获得特定文件名的键值的系统调用是key_t ftok(char* pathname, char proj);创建或打开一个消息队列的系统调用为int msgget(key_t key, int msgflg);其中msgflg可为IPC_CREATE,IPC_EXCL,IPC_NOWAIT,key参数(上面创建key时)设置成常数IPC_PRIVATE并不意味着其他进程不能访问该消息队列,只意味关即将创建新的消息队列,当调用msgget创建一个消息队列时,它相应的msqid_ds结构被初始化。 消息队列所传递的消息由两部分组成,即消息的类型和所传递的数据,一般用数据结构struct msgbuf来表示,通常消息类型是一个正的长整数,而数据则根据需要设定。发送:int msgsnd(int msqid, const void* ptr, size_t nbytes, int flags);其中flags指定消息队列满时的处理方法,如设置IPC_NOWAIT当消息队列没有足够的空间容纳要发送的消息时,立即出错返回,否则发送消息的进程被阻塞,造成阻塞可能是消息内容大或数量大(消息数目过多,但大小可能很小)。接收int msgrcv(int msqid, void* ptr, size_t nbytes, long type, int flags);可获取消息队列的属性,也可设置该数据结构:int msgctl(int msqid, int cmd, struct msqid_ds* buf);其中cmd:IPC_STAT,ICP_SET,IPC_RMID(表删除)。
(10.3)共享内存:它是存在于内核级别的一种资源。分布机制可让一个进程的物理地址不连续,也可让一段内存同时分配给不同的进程,共享内存机制就是通过该原理实现的。共享内存并不是读写数据后就解除映射,而是保持共享区域直到通信完毕,这样数据内容一直保存在共享内存中并没有写回文件,共享内存中的内容往往是在解除映射时才写回文件的,因为效率高。对每个共享存储段内核会为其维护一个shmid_ds结构体。int shmget(key_t key, int size, int flag); void* shmat(int shmd, const void* addr, int flag);失败返回-1,成功返回指向共享内存段的指针,且shmid_ds结构的shm_nattch计数值加1,int shmdt(void* addr);失败返回-1,成功返回0,int shmctl(int shmid, int cmd, struct shmid_ds* buf);失败返回-1成功返回0,其中cmd可IPC_STA,IPC_SET,IPC_RMID,SHM_LOCK,SHM_UNLOCK。
(10.4)信号量原理是一种数据操作锁的概念本身不具备数据交换的功能,而是通过控制其他通信资源(如文件、外部设备等)来实现进程间通信,它本身不具备数据传输的功能,只是一种外部资源的标识。在信号量上定义两种操作:Wait和Release。在信号量的实际应用中,是不能单独一个信号量的,只能定义一个信号量集,其中包含一组信号量,其中包含一组信号量同一信号量集中的信号量使用同一个引用ID,这样设置是为了多个资源或同步操作的需要,每个信号量集都有一个与之对应结构,记录了信号量集的各个信息。 int semget(key_t key, int nsems, int flags); int semop(int semid, struct sembuf semoparray[], size_t nops);成功返回0出错-1,此函数是原子操作。控制函数:int semctl(int semid, int semnum, int cmd, ...);成功是当操作为GET时返回相应值否则返回0,失败时返回-1,并设置errno。
十一、网络编程
物理层不是指物理设备或物理媒体而是有关物理设备通过物理媒体进行互连的描述和规定,它定义了接口的机械特性、电气特性、功能特性、规程特性4个基本特性,传输比特流。数据链路层确保帧间正确传输。网络层是包,负责网络寻址路由选择要考虑网络拥塞程度、服务质量、线路花费和线路有效性等,对数据进行分段和重组,以使得数据长度能满足数据链路层所支持的最大的数据帧(MTU)的长度。传输层单位是段,进行分段和重组(到网络层),会话层进行管理和控制保证会话数据可靠传送,它与传输层连接之间有三种关系,一对一、一对多、多对一,会话过程它需要决定使用全双工通信还是半双工通信,如果全双工会话层在对话管理中要做的工作就很少,如果半双工会话层通过一个数据令牌协调会话保证每次只有一个用户能够传输数据。表示层专门负责有关网络中计算机信息表示方式的问题,负责在不同数据格式之间数据转换操作、数据加密、压缩等。DNS属于应用层。
TCP负责和远程主机的连接,IP负责寻址。发送方将每个已发送的报文备份在自己的发送缓冲区里,且在收到相应的确认之前不会丢弃所保存的报文段的。滑动窗口协议是为了在端到端之间的实现流量控制,即匹配发送与接收的速度,而数据链路层的滑动窗口控制是为了相邻节点间实现流量控制。三次握手时源主机发送连接请求报文段其首部中SYN(同步)标志位置为1,并发送同步序列号X(如seq=100)表后面传送数据时的第一个数据字节的序号是X+1,目标主机同意连接在确认报文中应将ACK和SYN标志置为1,确认号应为X+1同时也应为自己选择一个序号Y,源主机收到后向目标主机给出确认,其ACK置为1,确认号为Y+1,而自己的序号为X+1。ACK是标志位,ack是确认序号(与发送信号对应)。TCP规定SYN置1的报文段要消息掉一个序号,当源主机向目标主机发送第一数据报文段时其序号仍为X+1因为其前一个确认报文段并不消耗序号。当一端请求关闭时发送FIN信号这时它不再发送,但依然可接收到消息(收到另一主机发的消息时还是要回馈自己收到了)。
套接口也就是网络进程的ID,网络通信归根到底还是进程间的通信。网络地址和端口号信息放在一个结构体中也就是套接口地址结构,大多数的套接口函数都需要一个指向套接口地址结构的指针作为参数,并以此来传递地址信息,套接口地址都以sockaddr_开关并以每个协议族名中的两个字母作为结尾,如IPv4对应的是sockaddr_in。端口号065535,其中01023为周知端口,在bind、connect等系统调用中特定于协议的套接口地址结构指针都要强转换成该通用的套接口地址结构指针以实现协议的无关性。端口与和IP地址都是以网络字节序存储的(大端模式),不一定同与主机字节序。htonl,htons返回网络字节序,ntohl,ntohs返回主机字节序,在套接口编程中,经常需要一起操作结构体中的某几个字节,就是用到字节操纵函数,有bzero,bcopy,bcmp,memset,memcpy,memcpy。其中以b开头的函数由任何支持套接口函数的系统所提供,以mem开关的函数由ANSI C提供。IP地址转换函数:int inet_aton(const char* straddr, struct in_addr* addrptr);成功返回1失败返回0,用于点分十进制转换为32位网络字节序,char* inet_ntoa(struct in_addr indaar);成功返回指向点分十进制数串的指针失败返回NULL,in_addr_t inet_addr(const char* straddr);成功返回32位二进制字节序地址,出错返回INADDR_NONE,IP和域名的转换struct hostent* gethostbyname是域名或主机名到IP地址的转换,gethostbyaddr与上相反。socket()系统调用为套接口在sockfs文件系统中分配一个新的文件和dentry对象,并通过文件描述符把它们与调用进程联系起来,进程可以像访问一个已打开的文件一样访问套接口在sockfs中的对应文件但不能调用open来访问(sockfs文件系统没有可视安装点,其中的文件永远不会出现在系统目录树上),当套接口被关闭时,内核会自动删除sockfs中的inodes。PF_INET与AF_INET是等同的,PF代表Protocol Family,AF代表Address Family。绑定时设置IP为INADDR_ANY表本地计算机地址。调用accept时参数sockfd表牌监听状态的socket,addr是一个sockaddr结构体类型的指针,系统会把远程主机的信息保存到这个指针所指向的结构体中,当accept函数接受一个连接时会返回一个新的socket标识符,int connect(int sockfd, const struct sockaddr* serv_addr, int addrlen)中serv_addr存储着远程服务器的IP与端口号信息,send成功返回已发送的字节数失败返回-1,成功时返回接收的字节数失败返回-1。
对于UDP服务端设置bind即可,就可发送接收了。当数据发送完毕后UDP套接口中的客户端调用close释放通信链路但不再发送“断开连接通知”来通知服务器端释放通信链路。int sendto(int sockfd, const void* msg, int len, unsigned int flags, struct sockaddr* toaddr, int* addrlen);成功返回实际发送的字节数,失败返回-1,int recvfrom(int sockfd, void* buf, int len, unsigned int flags, struct sockaddr* fromaddr, int* addrlen);成功返回实际接收的字节数失败返回-1。 原始套接口只能由有root权限的用户创建,如ping就使用原始套接口发送ICMP应答请求,创建sockfd,ping是应用层直接使用网络层ICMP的例子,它没有通过TCP或UDP。ICMP是IP层的一个协议,其报文需要通过IP协议来发送,发送前需要两级封装,首先添加其头,再添加IP头。网际校验和算法:把被校验的数据进行16位累加,然后取反,若数据字节长度为奇数则尾部补上一个字节的0以凑成偶数。
第12章 Linux图形界面编程
第13章 设计Linux下的计算器
第14章 Linux平台下聊天软件的设计
第15章 Linux远程管理工具的设计
第16章 Linux下简易防火墙软件的设计
第17章 基于Linux的嵌入式家庭网关远程文件交互操作
图书位置:
链接:https://pan.baidu.com/s/11bpTqIi0Julpd-p_LCuWZQ
提取码:a135