您的位置:首页 > 运维架构 > Linux

UNIX C 语言学习笔记

2014-09-23 10:18 369 查看
一、共享库

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互相锁定,死锁。

 
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  linux c语言 unix