Linux的IO和进程、线程

文章目录

  • IO进线程
    • 一、IO
      • 1)标准IO(C库提供)
        • 1.1 前言
        • 1.2 IO函数
          • 1.2.1 打开文件流`fopen()`
          • 1.2.2 关闭文件流`fclose()`
          • 1.2.3 读取文件
          • 1.2.4 文件流指针
          • 1.2.5 错误处理
      • 2)文件IO
        • 1.2.1 文件接口
          • 1.2.1.1 打开文件`open()`
          • 2.1.2 关闭文件`close()`
          • 1.2.1.3 读取内容`read()`
          • 1.2.1.4 写入内容`write()`
          • 1.2.1.5 文件定位`lseek()`
        • 1.2.2 目录接口
          • 1.2.2.1 目录的打开和关闭
          • 1.2.2.2 目录的读取
          • 1.2.2.3 文件的详细信息
          • 1.2.2.4 文件的用户名
          • 1.2.2.5 文件的组名
          • 1.2.2.6 获取链接文件的目标文件
          • 1.2.2.7 修改文件权限
      • 3)库
        • 1.3.0 静态库和动态库
        • 1.3.1 建立库
        • 1.3.2 使用库
        • 1.3.3 添加动态库路径
        • 1.3.4 gcc命令
    • 二、进线程
      • 1)进程
        • 2.1.1 进程控制块
        • 2.1.2 进程类型
        • 2.1.3 进程的状态
        • 2.1.4 进程相关命令
        • 2.1.5 创建进程
        • 2.1.6 结束进程
        • 2.1.7 回收进程
        • 2.1.8 exec函数族
        • 2.1.9 创建守护进程
        • 2.1.10 进程间的通信
          • 2.1.10.1 管道通信
          • 2.1.10.2 信号通信
          • 2.1.10.3 System V IPC
            • 2.1.10.3.1 System V 共享内存
            • 2.1.10.3.2 System V 消息队列
            • 2.1.10.3.3 System V 信号量
      • 2)线程
        • 2.2.1 创建线程
        • 2.2.2 结束单个线程
        • 2.2.3 回收某个线程
        • 2.2.4 线程间的同步和互斥
        • 2.2.5 线程间的通信
          • 2.2.5.1 信号量
          • 2.2.5.2 互斥锁
          • 2.2.5.2 互斥锁
    • 附录:进程内存空间图

IO进线程

文件:一组相关数据的有序集合。

一、IO

系统调用(文件描述符描述文件)

过程:上层应用向系统发出请求,系统响应并处理这个请求,并把请求的结果返回给上层应用

  • 用户空间进程访问内核的接口
  • 把用户从底层的硬件编程中解放出来
  • 极大的提高了系统的安全性
  • 使用户程序具有可移植性
  • 是操作系统的一部分

库函数(流来描述文件 stdin stdout stderr)

  • 库函数为了实现某个功能而封装起来的API集合。
  • 提供统一的编程接口,更加便于应用程序的移植
  • 是语言或者应用程序的一部分。

七大文件类型:

  1. - 普通文件: Linux中最多的一种文件类型

    包括 纯文本文件(ASCII);二进制文件(binary);数据格式的文件(data);各种压缩文件。

  2. d 目录文件:(directory)目录

  3. b 块设备文件:(block)存储数据以供系统存取的接口设备
    简单而言就是硬盘。例如一号硬盘的代码是dev/hda1等文件。

  4. c 字符设备文件:(chars)串行端口的接口设备
    例如键盘、鼠标等等。

  5. s 套接字文件:(socket) 用在网络数据连接
    这类文件通常可以启动一个程序来监听客户端的要求,客户端就可以通过套接字来进行数据通信。最常在 /var/run目录中看到这种文件类型

  6. p 管道文件:(pipeline)
    FIFO也是一种特殊的文件类型,它主要的目的是,解决多个程序同时存取一个文件所造成的错误。FIFO是first-in-first-out(先进先出)的缩写。

  7. l 链接文件:(link) 符号链接
    类似Windows下面的快捷方式。

1)标准IO(C库提供)

在系统调用接口之上封装的接口,一个C库函数可以封装多个系统调用函数。标准I/O又被称作缓存I/O。

特点:
1. 增强了代码的可移植性,复用性。
2. 提高了效率(减少系统调用时间)。

1.1 前言

标准IO增加了一个 【缓冲机制】

在内存空间,在输入输出设备之间存储数据,使低速的输入输出设备和高速的 CPU 工作相协调。

缓冲区类型

  1. 全缓冲(默认)在缓冲区满的时候进行写操作,在缓冲区空的时候进行读操作。
  2. 行缓冲(和终端相关的显示)
  3. 无缓冲(标准错误流没有缓冲区)

缓冲区刷新机制

  1. 缓冲区满的时候刷新
  2. **行缓冲遇见’\n’**时刷新
  3. 当文件正常结束时刷新
  4. 强制刷新fflush()函数
  5. 遇见输入函数时刷新

【流】(标准IO)

把数据的输入输出看作数据的流入流出。

流的类型:

  1. 文件流
  2. 二进制流(linux仅有二进制流)

系统默认打开的三种流

  1. 标准输入流 stdin 0 (文件描述符)
  2. 标准输出流 stdout 1
  3. 标准错误流 stderr 2

文件流指针 FILE结构体

每个被使用的文件都在内存中开辟一个区域,用来存放文件的有关信息,这些信息是保存在一个结构体类型的变量中,因此我们只需要用一个结构体指针指向内存中的地址就可以操作文件了。该结构体类型是由系统定义的,取别名为FILE。

1.2 IO函数
1.2.1 打开文件流fopen()
fopen()
//函数头
#include `
//函数原型
FILE *fopen(const char *filename, const char *mode);`
    //传参:文件名称(文件路径)、打开后的权限
    //返回值:文件指针
1) 文件使用方式由r,w,a,t,b,+六个字符拼成,各字符的含义是:
    r(read):w(write):a(append): 追加
    t(text): 文本文件,可省略不写
    b(banary): 二进制文件
    +: 读和写
2) 凡用“r”打开该文件必须已经存在
3) 文件新建的权限:
        fopen默认创建的权限为0666,最终的权限为 0666 & ~umask值(权限掩码);
模式 描述 文件可否存在
“r” 仅可读 必须存在
“r+” 读+写 必须存在
“w” 清空写入 可以不存在
“w+” 读+清空写入 可以不存在
“a” 追加写入(始终在从末尾写) 可以不存在
“a+” 读+追加写入(始终在从末尾写) 可以不存在
特点 打开文件时文件的位置指针的偏移量为0
1.2.2 关闭文件流fclose()

流的打开是有个数限制的,linux最多可以打开1021个文件流(因为默认打开了stdin,stdout,stderr),因此在使用流之后就得关闭。

fclose()
//此动作会让缓冲区内的数据写入文件中,并释放系统所提供的文件资源。
#include 
/*@msg: stream文件指针句柄
* @return:如果流成功关闭,则该方法返回0;如果失败,则返回 EOF;
*/    
int fclose(FILE *stream)
1.2.3 读取文件

单个字符

fgetc()//从一个流中获取一个字符
	int fgetc(FILE *filename);
	//在文件处理中,通过fgetc()函数,我们从输入流中获取下一个字符,并将文件指针加1
fputc()//从一个流中输出一个字符
    int fputc ( int ch, FILE *fp );
	//向指定的文件中写入一个字符

按行读取

使用 fgets(buf,sizeof(buf),stdin)代替guts()时:

当输入的数据个数小于 size - 1,会获得在终端的\n;
在输入的数据大于等于 size - 1 时,只能读取 size - 1 个字符,必定会留一个位置给\0,没有读取的内容保留在输入缓冲区。

//fgets()
#include 
char *fgets(char *s, int size, FILE *stream);
@msg:
	//s 代表要保存到的内存空间的首地址,可以是字符数组名,也可以是指向字符数组的字符指针变量名。
    //size 代表最大能读取 size - 1 个字符。
	//stream 表示从何种流中读取。
@return//成功:返回指向该串的指针;失败或读到文件结尾 返回空指针.
    
//fputs()
#include 
int fputs(const char *s, FILE *stream);
@msg:
	//字符串指针
    //stream 表示输出到何种流。
@return//成功则返回非负数

按对象

//fread()
#include 
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
@msg:
	*ptr: 要读到的内存地址;
    size: 读取的 基本单元 字节大小,单位是字节,一般是 ptr 缓冲的单位大小;
        如果 ptr 缓冲区是 char 数组,则该参数的值是 sizeof(char);
        如果 ptr 缓冲区是 int 数组,则该参数的值是 sizeof(int);
    nmemb: 读取的基本单元个数;
    *stream: 文件流指针;
@returnsize_t : 实际从文件中读取的基本单元个数 ; 读取的字节数是基本单元数 乘以 基本单元字节大小 ;

//fwrite()
size_t fwrite(const void *ptr, size_t size, size_t nmemb,FILE *stream);
@msg:
    *ptr : 指针指向要写出数据的内存首地址 ;
    size : 要写出数据的 基本单元 的字节大小 , 写出单位的大小 ;
    nmemb : 要写出数据的 基本单元 的个数 ;
    *stream : 打开的文件指针 ;
@returnsize_t 返回值返回的是实际写出到文件的 基本单元 个数 ;
1.2.4 文件流指针

查看指针位置

ftell() 函数的作用是 获取文件的 当前指针位置 相对于 文件首地址 的 偏移字节数 ;

ftell()

#include 
long ftell(FILE *stream);

@return
    //当前指针位置 相对于 文件首地址 的 偏移字节数;

把文件的位置指针指向开头

rewind()

void rewind(FILE *stream);

文件定位

文件位置宏

  • #define SEEK_SET 0 // 文件开头
  • #define SEEK_CUR 1 //当前位置
  • #define SEEK_END 2 //文件结尾
fseek()

int fseek(FILE *stream, long offset, int whence);
@msg
    *stream :文件指针
	offset :为偏移量,整数表示正向偏移,负数表示负向偏移
	whence :设定从文件的哪里开始偏移,可能取值为:SEEK_CURSEEK_ENDSEEK_SET
@return
    int :成功,返回0,否则返回其他值
1.2.5 错误处理
perror()
//将上一个函数发生错误的原因输出到标准设备(stderr)
//可以输入errno命令看看有没有errno工具
#include
void perror(const char *s);//输出error值对应的错误信息
char *strerror(int errnum);//将错误码以字符串的信息显示出来
//在linux使用errno -l命令查看系统错误代码及描述

EOF 本来表示文件末尾,意味着读取结束,但是很多函数在读取出错时也返回 EOF,那么当返回 EOF 时,到底是文件读取完毕了还是读取出错了?

我们可以借助 stdio.h 中的两个函数来判断,分别是 feof()ferror()

feof() 函数用来判断文件内部指针是否指向了文件末尾,它的原型是:

int feof ( FILE * fp );		//未到末尾返回值为0

当指向文件末尾时返回非零值,否则返回零值。

ferror() 函数用来判断文件操作是否出错,它的原型是:

int ferror ( FILE *fp );	//未出错返回0

出错时返回非零值,否则返回零值。


2)文件IO

  1. 操作系统直接提供的函数接口
  2. 调用系统调用是很耗费资源的
1.2.1 文件接口
1.2.1.1 打开文件open()
open();
//函数头
#include 
#include 
#include 
//函数原型
int open(const char *pathname, int flags);	//打开已有文件
@msg
    1. *pathname: 文件路径及文件名;
	2. flags: 打开方式:
		O_RDONLY	//只读
        O_WRONLY	//只写
        O_RDWR		//读写
@return
    int: 打开成功返回该文件的文件描述符;

int open(const char *pathname, int flags, mode_t mode);
@msg
    1. *pathname: 文件路径及文件名;
	2. flags: 打开方式:
		O_RDONLY	//只读
        O_WRONLY	//只写
        O_RDWR		//读写
            
        O_CREAT		//创建并赋予权限     
        O_APPEND	//追加
        O_TRUNC		//覆盖    
        O_EXCL		//判断文件是否存在(一般和O_CREAT连用,表示不存在则创建,存在则返回-1,表示文件打开失败)
    3.mode:和O_CREAT连用,用于记录待创建的文件的访问权限(依然和权限掩码有关)
@return
    int: 打开成功返回该文件的文件描述符;
2.1.2 关闭文件close()

linux 下文件描述符打开的次数是有限制的,(1024个)使用完以后需要关闭;

close()
#include 

int close(int fd);
关闭文件:
    成功返回0,失败返回-1并设置错误号。
1.2.1.3 读取内容read()
#include 

ssize_t read(int fd, void *buf, size_t count);
@msg
	fd:文件描述符
    *buf:要写入的内容首地址;
    count:要写入的数据数量
@return
    ssize_t:写入的数量
1.2.1.4 写入内容write()
#include 

ssize_t write(int fd, const void *buf, size_t count);
@msg
    fd:文件描述符
    *buf:要写入的内容首地址;
    count:要写入的数据数量
@return
    ssize_t:写入的数量
1.2.1.5 文件定位lseek()
#include 
#include 

off_t lseek(int fd, off_t offset, int whence);
@msg
    1. fd :文件描述符
	2. offset :为偏移量,整数表示正向偏移,负数表示负向偏移
	3. whence :设定从文件的哪里开始偏移,可能取值为:SEEK_CURSEEK_ENDSEEK_SET
@return
    off_t1. 成功返回当前位置到开始的长度
    	2. 失败返回-1并设置errno

1.2.2 目录接口

想要获取某目录下(比如include目录下)stdio.h文件的详细信息,我们应该怎样做?

  1. 首先,我们使用opendir函数打开目录include,返回指向目录include的DIR结构体pf。
  2. 接着,我们调用readdir(pf)函数读取目录include下所有文件(包括目录),返回指向目录include下所有文件的dirent结构体d。
  3. 然后,我们遍历d,调用stat(d->name,stat *e)来获取每个文件的详细信息,存储在stat结构体e中。
    总体就是这样一种逐步细化的过程,在这一过程中,三种结构体扮演着不同的角色。
1.2.2.1 目录的打开和关闭
#include 
#include 
DIR *opendir(const char *name);
{
    @msg
 		name: 目录路径;
    @return
  		DIR *: 成功返回一个目录流指针,失败返回NULL并设置错误号;
}

int closedir(DIR *dirp);	//关闭目录流
{
    @msg
        dirp:目录流指针
    @return
        关闭成功为0,失败为-1,并设置errorno。
}
//DIR 结构体
struct __dirstream   
{   
    void *__fd;    
    char *__data;    
    int __entry_data;    
    char *__ptr;    
    int __entry_ptr;    
    size_t __allocation;    
    size_t __size;    
    __libc_lock_define (, __lock)    
};   
typedef struct __dirstream DIR;
1.2.2.2 目录的读取
#include 

struct dirent *readdir(DIR *dirp);
@msg
    dirp:目录流指针
@return
    成功时返回目录结构体;失败或者读完时返回NULL,失败时设置错误号(errorno)。
    
struct dirent	//目录内容
{
   long d_ino; 					/* inode number 索引节点号 */
   off_t d_off; 				/* offset to this dirent 在目录文件中的偏移 */
   unsigned short d_reclen; 	/* length of this d_name 文件名长 */
   unsigned char d_type; 		/* the type of d_name 文件类型 */
   char d_name [NAME_MAX+1]; 	/* file name (null-terminated) 文件名,最长255字符 */
}
1.2.2.3 文件的详细信息

st_mode是用特征位来表示文件类型的,特征位的定义如下:

描述
S_IFMT 0170000 文件类型的位遮罩
S_IFSOCK 0140000 socket
S_IFLNK 0120000 符号链接(symbolic link)
S_IFREG 0100000 一般文件
S_IFBLK 0060000 区块装置(block device)
S_IFDIR 0040000 目录
S_IFCHR 0020000 字符装置(character device)
S_IFIFO 0010000 先进先出(fifo)
S_ISUID 0004000 文件的(set user-id on execution)位
S_ISGID 0002000 文件的(set group-id on execution)位
S_ISVTX 0001000 文件的sticky位
S_IRWXU 00700 文件所有者的遮罩值(即所有权限值)
S_IRWXG 00070 用户组的遮罩值(即所有权限值)
S_IRWXO 00070 其他用户的遮罩值(即所有权限值)

文件的权限

第n位 文件权限宏 八进制 描述
678 S_IRWXU 00700 文件所有者的遮罩值(即所有权限值)
8 S_IRUSR 00400 文件所有者具可读取权限
7 S_IWUSR 00200 文件所有者具可写入权限
6 S_IXUSR 00100 文件所有者具可执行权限
345 S_IRWXG 00070 用户组的遮罩值(即所有权限值)
5 S_IRGRP 00040 用户组具可读取权限
4 S_IWGRP 00020 用户组具可写入权限
3 S_IXGRP 00010 用户组具可执行权限
012 S_IRWXO 00007 其他用户的遮罩值(即所有权限值)
2 S_IROTH 00004 其他用户具可读取权限
1 S_IWOTH 00002 其他用户具可写入权限
0 S_IXOTH 00001 其他用户具可执行权限
#include 
#include 
#include 

int stat(const char *pathname, struct stat *statbuf);
{
	@msg
        *pathname:文件名和路径;
        *statbuf:文件的详细内容存放在statbuf指向的stat结构体中;
    @return
        int:成功返回0,失败返回-1;
}
int fstat(int fd, struct stat *statbuf);
{
 	   
}
struct stat		//文件的详细信息
{   
    dev_t       st_dev;     /* ID of device containing file -文件所在设备的ID*/  
    ino_t       st_ino;     /* inode number -inode节点号*/    
    mode_t      st_mode;    /* protection -文件访问权限*/    
    nlink_t     st_nlink;   /* number of hard links -链向此文件的连接数(硬连接)*/    
    uid_t       st_uid;     /* user ID of owner -user id*/    
    gid_t       st_gid;     /* group ID of owner - group id*/    
    dev_t       st_rdev;    /* device ID (if special file) -设备号,针对设备文件*/    
    off_t       st_size;    /* total size, in bytes -文件大小,字节为单位*/    
    blksize_t   st_blksize; /* blocksize for filesystem I/O -系统块的大小*/    
    blkcnt_t    st_blocks;  /* number of blocks allocated -文件所占块数*/    
    time_t      st_atime;   /* time of last access -上次存取时间*/    
    time_t      st_mtime;   /* time of last modification -上次修改时间*/    
    time_t      st_ctime;   /* time of last status change - 创建时间*/    
};
1.2.2.4 文件的用户名
#include 
#include 

struct passwd *getpwnam(const char *name);
struct passwd *getpwuid(uid_t uid);


struct passwd 
{
    char   *pw_name;       /* username */
    char   *pw_passwd;     /* user password */
    uid_t   pw_uid;        /* user ID */
    gid_t   pw_gid;        /* group ID */
    char   *pw_gecos;      /* user information */
    char   *pw_dir;        /* home directory */
    char   *pw_shell;      /* shell program */
};

1.2.2.5 文件的组名
#include 
#include 
struct group *getgrnam(const char *name);
struct group *getgrgid(gid_t gid);

struct group 
{
    char   *gr_name;        /* group name */
    char   *gr_passwd;      /* group password */
    gid_t   gr_gid;         /* group ID */
    char  **gr_mem;         /* NULL-terminated array of pointersto names of group members */
};
1.2.2.6 获取链接文件的目标文件
#include 
ssize_t readlink(const char *pathname, char *buf, size_t bufsiz);
{
    @msg
        *pathname:符号链接文件名
    	*buf :存储参数path的符号链接的内容。
    	bufsiz :buf的长度
    @return
        ssize_t :执行成功则返回字符串的字符数,失败返回-1, 错误代码存于errno。
}
//若参数bufsiz小于符号连接的内容长度,过长的内容会被截断,如果 readlink 第一个参数指向一个文件而不是符号链接时,readlink 设 置errno 为 EINVAL 并返回 -1。
1.2.2.7 修改文件权限
#include 

int chmod(const char *pathname, mode_t mode);	//通过文件名修改权限
{
	@return
		成功返回0,失败为-1并设置errorno。
}
int fchmod(int fd, mode_t mode);	//通过文件描述符
{
	@return
		成功返回0,失败为-1并设置errorno。
}

3)库

本质上来说库是一种可执行代码的二进制形式,可以被操作系统载入内存执行。

由于windows和linux的本质不同,因此二者库的二进制是不兼容的。

linux下的库有两种:静态库和共享库(动态库)。

​ 二者的不同点在于代码被载入的时刻不同

  • 静态库在程序编译时,会被连接到目标代码中,程序运行时将不再需要该静态库,因此体积较大

  • 动态库在程序编译时并不会被连接到目标代码中,而是在程序运行时才被载入,因此在程序运行
    时还需要动态库存在,因此代码体积较小

库是别人写好的现有的,成熟的,可以复用的代码,你可以使用但要记得遵守许可协议。现实中每个程序都要依赖很多基础的底层库,不可能每个人的代码都从零开始,因此库的存在意义非同寻常。

共享库的好处是,不同的应用程序如果调用相同的库,那么在内存里只需要有
一份该共享库的实例。

1.3.0 静态库和动态库

静态库对函数库的链接是放在编译时期(compile time)完成的。

  • 优点:程序在运行时与函数库再无瓜葛,移植方便。

  • 缺点:浪费空间和资源,因为所有相关的对象文件(object file)与牵涉到的函数库(library)被链接合成一个可执行文件(executable file)。

库的命名规则

静态库:lib + 库名 + .a

动态库:lib + 库名 + .so

1.3.1 建立库

将源文件转换为目标文件

-> gcc -c 源文件 -o 目标文件	#静态库的编译
-> gcc -fPIC -c func2.c -o func2.o		#动态库的编译

静态库的建立

ar crs -o 库文件名 目标文件列表

动态库的建立

gcc -fPIC -c func1.c -o func1.o
gcc -fPIC -c func2.c -o func2.o
	-FPIC 生成位置无关代码
gcc -shared -o libmylib.so func1.o func2.o
1.3.2 使用库

静态库的使用

库的使用:
		gcc main.c -L ./(库的路径) -lmylib (-l后加库名) -static
   或者:
		gcc main.c libmylib.a (直接加库文件名)
      
  当动态库和静态库同时存在的时候,优先链接动态库,如果要使用静态库,在编译的末尾加上 -static;

动态库的使用

库的使用:
		gcc main.c -L ./(库的路径) -lmylib (-l后加库名)
   或者:
		gcc main.c libmylib.so (直接加库文件名)
1.3.3 添加动态库路径
程序运行的时候出现如下错误:
  ./a.out: error while loading shared libraries: libmylib.so: cannot open shared object file: No such file or directory

错误原因:程序运行的时候会去链接动态库,找不到库(默认路径在 /lib 、/usr/lib)
  • 解决办法:
    ① 把库文件名拷贝到/lib 或者 /usr/lib
    ② 导入一个临时环境变量:
    export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:./
    ③ 新建一个文件: sudo vi /etc/ld.so.conf.d/mylib.conf
    在此文件中添加库的绝对路径;sudo ldconfig 生效;

LD_LIBRARY_PATH: 动态库的查找路径

ldconfig的主要用途

ldconfig是一个动态链接库管理命令,其目的为了让动态链接库为系统所共享

  1. 默认搜寻/lilb和/usr/lib,以及配置文件/etc/ld.so.conf内所列的目录下的库文件

  2. 搜索出可共享的动态链接库,库文件的格式为:lib***.so.**,进而创建出动态装入程序(ld.so)所需的连接和缓存文件。

  3. 缓存文件默认为/etc/ld.so.cache,该文件保存已排好序的动态链接库名字列表。

  4. ldconfig通常在系统启动时运行,而当用户安装了一个新的动态链接库时,就需要手工运行这个命令


1.3.4 gcc命令
  1. 预处理(宏替换、头展开)
-> gcc -E (源文件名) -o (预处理文件名)	# .i
-> gcc (源文件名) > (预处理文件名)
  1. 编译(检查语法是否错误)
-> gcc -S (源文件) -o (汇编文件)		# .S
# 此步骤也是编译程序中,最耗时,因为需要逐行检查语法
  1. 汇编(转为二进制码)
-> gcc -c (汇编文件) -o (目标文件)		# .o
  1. 链接(生成可执行)
-> gcc -c [目标文件] -o [可执行程序] -l[动态库名]	# .out
#俩个步骤
    1. 地址回填
    2. 数据段合并

一步到位

-> gcc -o test test.c

增加链接库路径 -L

-> gcc -c [目标文件] -o [可执行程序] -L[静态库路径] -l[静态库名] 

指定链接静态库(默认链接动态库) -static

-> gcc -c [目标文件] -o [可执行程序] -L[静态库路径] -l[静库名] -static

生成位置无关代码 -fPIC

使用-fPIC产生的代码中,没有绝对地址,全部使用相对地址,故而代码可以被加载器加载到内存的任意位置,都可以正确的执行。这正是共享库所要求的,共享库被加载时,在内存的位置不是固定的。

  • 该库可能需要经常更新

  • 该库需要非常高的效率(尤其是有很多全局量的使用时)

  • 该库并不很大.

  • 该库基本不需要被多个应用程序共享

有以上条件就不需要加 -fPIC

-> gcc -fPIC -c sub.c -o sub.o 

生成共享库文件 -shared

-> gcc -shared -o libsub.so sub.o sub2.o sub3.o 
				  #共享库名	 链接文件

增加头文件路径 -I

-> gcc -c [目标文件] -o [可执行程序] -I ../inlcude

二、进线程

1)进程

程序是静态的,它是一些保存在磁盘上的指令的有序集合,没有任何执行的概念
进程是一个动态的概念,它是程序执行的过程,包括创建、调度和消亡。

进程是一个程序的一次执行的过程,是资源管理的最小单位,是一个独立的可调度的任务。

进程:

①程序:
正文段(代码段)

用户数据段

②系统数据段

PCB(进程控制块)、PC(程序计数器)、堆栈、相关寄存器

堆栈——存放的是函数的返回地址、函数的参数以及程序中的局部变量

PC(程序计数器):每个进程会获得时间片切换运行。PC会记录每个进程的执行状态。


2.1.1 进程控制块
  • 进程控制块(task_struct)
    • ​ 进程标识PID
    • ​ 进程用户
    • 文件描述符(记录当前打开的文件)
    • ​ 进程状态、优先级(记录调度顺序、分配的时间片的长短

2.1.2 进程类型
  • 交互进程:该类进程是由shell控制和运行的。交互进程既可以在前台运行,也可以在后台运行。(终端挂钩

  • 批处理进程:该类进程不属于某个终端,它被提交到一个队列中以便顺序执行。(系统管理员使用)

  • 守护进程:后台进程、开机时启动关机时结束。(一般用于服务器)

一个运行起来的程序被称为进程,在Linux中有些进程不与任何进程关联,不论用户的身份如何,都在后台运行。这些进程的父进程是PID为1的进程,PID为1的进程只有在系统关闭时才会被销毁。它会在后台一直运行等待分配工作,我们将这类进程称之为守护进程。

守护进程的名字通常会在最后有一个d,表示daemon守护的意思,例如systemdhttpd


2.1.3 进程的状态

运行态 R :此时进程或者正在运行,或者准备运行。
等待态:此时进程在等待一个事件的发生或某种系统资源。

  • 可中断 S(eg: getchar( ))
  • 不可中断 D(操作硬件的时候)

停止态 T:此时进程被中止。
僵尸态 Z:这是一个已终止的进程,但还在进程向量数组中占有一个task_struct结构。

状态 描述
R 运行状态 进程不一定在运行,表明进程要么在运行中,要么在运行队列里。
S 睡眠状态 可中断睡眠,进程在等待事件完成,此时进程处于等待队列。
D 磁盘休眠状态 不可中断睡眠状态,在这个状态的进程通常会等待IO的结束。
T 暂停状态 发送 SIGSTOP 信号停止进程。发送 SIGCONT 信号让进程继续运行。
X 死亡状态 这个状态只是一个返回状态,你不会在任务列表里看到这个状态。
Z 僵尸进程 僵尸进程会以终止状态保持在进程表中,并且一直等待父进程读取退出状态代码
孤儿进程 父进程如果提前退出,子进程就会成为孤儿进程,孤儿进程会被1号init进程领养。
S 父进程
+ 前台进程
> 高优先级
N 低优先级

僵尸状态是一个比较特殊的状态,当进程退出并且父进程没用读取到子进程的退出码时,就会产生僵尸状态。

一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵死进程。


2.1.4 进程相关命令

查看进程

ps			#查看当前终端的进程
ps -aux		#以用户为主,显示当前用户在当前终端的所有进程
ps -elf		#以完整的长格式显示系统内所有进程

ps -aux | grep [进程名]

ps -aux 显示的内容如下

描述
USER 进程所属人
PID 进程id
%CPU cpu使用量
%MEM 内存使用量
VSZ 虚拟内存的大小
RSS 常驻内存的使用大小
TTY 进程使用到的终端
STAT 进程状态
START 进程运行时长
TIME 进程占用cpu时长
COMMAND 进程名称
top		#以全屏交互式的界面显示进程,每三秒刷新一次
top -c 	#显示完整的命令路径
top -d 	#<时间> 设置间隔时间
top -u 	#<用户名> 指定用户名
top -p 	#<进程号> 指定进程
top -n 	#<次数> 循环显示的次数
top -d 1 -c -p 12	#每隔1秒显示pid是12的进程的资源使用情况,并显示该进程启动的命令行参数

#在top基本视图,按
f	#编辑基本视图中的显示字段
c	#显示进程的路径
k	#不退出top命令的情况下杀死某个正在运行的进程
b	#高亮显示当前正在运行的进程
1	#监控每个逻辑CPU的状况
pstree -aup		#以树状图的方式展现进程之间的派生关系,显示效果比较直观
jobs	#查看依附于当前终端的后台进程

bg		#将挂起的进程在后台重新运行
fg		#将后台进程放在前台运行
eg: bg 1	#将1号任务在后台运行 

Linux的IO和进程、线程_第1张图片

进程的优先级

PRI		#代表这个进程可被执行的优先级,其值越小越好,默认值都是80
NI		#用户通过调整NICE值修改进程的优先级,而NICE的取值范围为(-20~19),越小越高。


nice		#按用户的指定优先级运行进程
renice		#修改进程的nice值

向进程发送信号

kill -l		#查看信号种类

kill -9 [PID]	#向进程发送SIGKILL信号

2.1.5 创建进程
#include 
#include 

pid_t fork(void);	//创建进程
{
	@return
        pid_t: 
    		1)在父进程中,fork返回新创建子进程的进程ID;
    		2)在子进程中,fork返回03)如果出现错误,fork返回一个负值;
}
可以将fork函数分为三步
	1.调用_CREATE函数,也就是进程创建部分
    2.调用_CLONE函数,也就是资源拷贝部分
    3.进程创建成功,return 0;     失败,return -1:
  • 实质:

​ 父进程中在调用fork()派生新进程,实际上相当于创建了进程(进程包括正文段、用户数据段、系统数据段)的一个不完全拷贝(系统数据段的 PID 需要自己申请);即在fork()之前的进程拥有的资源会被复制到新的进程中去。

  • 用法
  1. 一个进程进行自身的复制,这样每个副本可以独立的完成具体的操作,在多核处理器中可以并行处理数据。这也是网络服务器的其中一个典型用途,多进程处理多连接请求。
  2. 一个进程想执行另一个程序。比如一个软件包含了两个程序,主程序想调起另一个程序的话,它就可以先调用fork来创建一个自身的拷贝,然后通过exec函数来替换成将要运行的新程序。

2.1.6 结束进程
#include 
void exit(int status);		//C库函数
{
    @msg
        status: 一个8位的整型数。(-255~+255;
}// 清理IO缓存 -> 调用退出处理函数 -> 调用系统调用_exit() -> 进程结束

#include 
void _exit(int status);		//系统调用
{
    @msg
        status: 一个8位的整型数。(-255~+255;
}

exit()和_exit()的差异

  1. exit函数在调用_exit()系统调用退出进程之前会调用退出处理函数,但_exit()函数并不会调用,直接退出

  2. exit会清理IO缓存,如果缓冲区有数据,他会把数据刷新回设备;而 _exit()函数不会这么做,缓存数据会直接丢失。


2.1.7 回收进程
#include 
#include 
//wait()要与fork()配套出现。
pid_t wait(int *wstatus);	//阻塞自己并等待任意子进程结束,并回收其资源。
{
    @msg
        *wstatus: 指向子进程退出状态的指针
    @return
        pid_t: 正常时返回已结束的子进程PID,失败时返回 -1、并设置errno为ECHILD。
}
//当父进程忘了用wait()函数等待已终止的子进程时,子进程就会进入一种无父进程的状态,此时子进程就是僵尸进程。


#include 
#include 
pid_t waitpid(pid_t pid, int *wstatus, int options);//
{
	@msg
        1. pid: 
    		pid∈(-,-1) : 等待进程组号为abs(pid)的任何子进程结束
            pid == -1 : 表示回收|等待 任意子进程结束
            pid == 0 : 等待进程组号与目前进程相同的任何子进程,也就是说任何和调用waitpid()函数的进程在同一个进程组的进程。
    		pid ∈[1,+) : 等子进程PID为pid的进程结束。
    	2. *wstatus: 指向子进程退出状态的指针
    	3. options: 
        	0 - 阻塞回收 
            WNOHANG - 非阻塞回收(没有进程结束,返回0)
    @return 
        1. pid_t : 正常时返回已结束的子进程PID,失败时返回 -1、并设置errno为ECHILD。 
}

waitpid() 的 第一个参数

参数值 说明
pid<-1 等待进程组号为pid绝对值的任何子进程。
pid=-1 等待任何子进程,此时的waitpid()函数就退化成了普通的wait()函数。
pid=0 等待进程组号与目前进程相同的任何子进程,也就是说任何和调用waitpid()函数的进程在同一个进程组的进程。
pid>0 等待进程号为pid的子进程。

判断状态的宏函数

说明
WIFEXITED(status) 如果子进程正常结束,它就返回真;否则返回假。
WEXITSTATUS(status) 如果WIFEXITED(status)为真,则可以用该宏取得子进程exit()返回的结束代码。
WIFSIGNALED(status) 如果子进程因为一个未捕获的信号而终止,它就返回真;否则返回假。
WTERMSIG(status) 如果WIFSIGNALED(status)为真,则可以用该宏获得导致子进程终止的信号代码。
WIFSTOPPED(status) 如果当前子进程被暂停了,则返回真;否则返回假。
WSTOPSIG(status) 如果WIFSTOPPED(status)为真,则可以使用该宏获得导致子进程暂停的信号代码。

waitpid() 的 第三个参数

参数 说明
WNOHANG 如果pid指定的子进程没有结束,则waitpid()函数立即返回0,而不是阻塞在这个函数上等待;如果结束了,则返回该子进程的进程号。
WUNTRACED 如果子进程进入暂停状态,则马上返回。
WCONTINUED 如果被停止的子进程已经通过SIGCONT信号被恢复,也返回。

2.1.8 exec函数族

fork函数用于创建一个子进程,该子进程几乎拷贝了父进程的全部内容。

为了使子进程能够重新去执行一个新的程序,exec函数族提供了一种在进程中启动另一个程序执行的方法。

它可以根据指定的文件名或目录名找到可执行文件,并用它来取代原调用进程的数据段、代码段和堆栈段。

在执行完之后,原调用进程的内容除了进程号外,其他全部都被替换了。

#include 

extern char **environ;	//环境变量表

int execl(const char *pathname, const char *arg, .../* (char  *) NULL */);
int execlp(const char *file, const char *arg, .../* (char  *) NULL */);
int execle(const char *pathname, const char *arg, .../*, (char *) NULL, char *const envp[] */);
int execv(const char *pathname, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[],char *const envp[]);

返回值:
  exec函数族的函数执行成功后不会返回,调用失败时,会设置errno并返回-1,然后从原程序的调用点接着往下执行。
参数说明:

  • path:可执行文件的路径名字
  • arg:可执行程序所带的参数,第一个参数为可执行文件名字,没有带路径且arg必须以NULL结束
  • file:如果参数file中包含/,则就将其视为路径名,否则就按 PATH环境变量,在它所指定的各目录中搜寻可执行文件。

exec族函数参数极难记忆和分辨,函数名中的字符会给我们一些帮助:

  • l : 使用参数列表

  • v:(矢量vector应先构造一个指向各参数的指针数组,然后将该数组的地址作为这些函数的参数。

    l 和 v 不会一起出现,因为他们都是命令的参数,只能选择其中一种方式。

  • p:使用文件名,并从PATH环境进行寻找可执行文件

    如果filename中包含/,则将视为路径名;

    如果不包含/,则按照PATH环境变量,在它所指定的各个目录中搜寻可执行文件。

  • e:多了envp[]数组,使用新的环境变量代替调用进程的环境变量

    一个进程允许将其中环境传播给器子程序,但有时也会有,进程想要为子进程指定某一个确定的环境。


2.1.9 创建守护进程
  1. 特点
  • 后台进程
  • 独立于终端(与交互进程最大的区别)
  • 周期性的执行一些任务或者处理一些事件
  • 生命周期长,一般守护进程从系统启动时开始运行,到系统关闭时结束
  1. 会话组、进程组、进程

Linux中为了更好的管理进程,就有了进程组和会话组的划分;

  1. 会话组

    会话组是一个或多个进程组的集合。
    通常用户打开一个终端时,系统会创建一个会话。
    所有通过该终端运行的进程都属于这个终端。

    当终端打开时,默认执行shell进程;

    系统会创建一个会话,在这个会话中的第一个进程为shell进程,将shell进程称为会话的首进程,也把shell进程称为会话组的组长。

    在shell下执行的所有程序,创建的所有的进程都属于同一个会话,把这个终端称为会话的控制终端;

  2. 进程组

    当运行起一个程序的时候,实际上系统就创建了一个进程来执行这个程序。在进程创建的同时创建了一个新的进程组。即每个运行的程序对应一个进程组。如果在这个程序中创建了子进程,实际上子进程和父进程一样,属于同一个进程组。

    每个进程都属于一个进程组;

    进程组是一个或多个进程的集合。进程组由进程组ID来唯一标识。
    每个进程组都有一个组长进程,进程组ID就是组长进程的进程号

  1. 注意

    a. 一个会话最多只能打开一个控制终端。可以不打开,但是要打开的话,最多只能打开一个控制终端。

    b.控制终端对会话的影响:系统规定,当一个控制终端关闭时,所有依附于该终端的会话中的所有的进程都会被结束。

  1. 创建守护进程编程步骤

    step 步骤 函数 描述
    1 创建子进程,父进程退出 fork() 形式上脱离终端
    2 在子进程中创建新会话 setsid() 子进程完全脱离终端 属于新会话组的组长
    3 改变当前目录为根目录 chdir(“/”) 守护进程的工作目录不能被随意卸载
    4 重设文件权限掩码 umask(0) 文件实际权限:mode & ~umask
    5 关闭文件描述符 close(fd) 关闭fork时从父进程拷贝过来的所有文件

2.1.10 进程间的通信
进程通信方式
管道
信号
signal
system V
网络通信
内核向用户进程通信
匿名管道
命名管道
单向 亲缘 半双工
两个通信进程之一只能使用标准输入和标准输出 则无法使用FIFO
共享内存
消息队列
信号量
效率最高
socket
类型
同步通信
异步通信
  • 管道通信

    管道是Linux中很重要的一种通信方式,是把一个程序的输出直接连接到另一个程序的输入,从管道读数据是一次性操作,数据一旦被读,它就从管道中被抛弃,释放空间以便写更多的数据。

    • 匿名管道

      无名管道只能用于具有亲缘关系的进程之间的通信,通信属于半双工

    • 命名管道

      有名管道叫named pipe或者FIFO(先进先出),可以用函数mkfifo()创建。

  • system V 标准

    • 共享内存
    • 消息队列
    • 信号量
  • 网络通信

同步通信与异步通信

  • 同步通信:发送方发送数据,接收方接收数据,双方在很短的时间内完成数据的交换,否则会造成一方的阻塞。所以同步通信是一种阻塞模式的通信。

  • 异步通信:通信中接收方并不知道数据什么时候会到达,当前进程一直准备接收数据,同时也在做自己的事情,一旦数据到达立即接收处理。


2.1.10.1 管道通信

​ 进程A中的数据写入到内核中,进程B中的数据也写入到内核中,两者在内核中进行交换。交换后,进程A读取内核中的数据,进程B也读取内核中的数据,这样两个进程间交换数据的通信就完成了。两个进程通过内核建立了联系,那么交换数据、传递数据、发送事件等行为就都可以实现了。

无名管道

#include 
//创建管道

/* On Alpha, IA-64, MIPS, SuperH, and SPARC/SPARC64; see NOTES */
struct fd_pair {
	long fd[2];
};
struct fd_pair pipe();

/* On all other architectures */
 int pipe(int pipefd[2]);
{
    @msg
    	pipefd :指向含有两个整形数组的指针;用于保存管道的读写(pipefd[0],pipefd[1])的文件描述符
    @return
        int 成功,返回0;失败返回-1,并且设置了适当的错误返回信息。
}

pipe()函数用于在内核中创建一个管道,该管道一端用于读取管道中的数据,另一端用于将数据写入管道。

​ 在创建一个管道后,会获得一对文件描述符,用于读取和写入,然后将参数数组filedes中的两个值传递给获取到的两个文件描述符,filedes[0]指向管道的读端,filedes[1]指向写端。

创建管道后,需要使用read()和write()函数来完成。当管道通信结束后,需要使用close()函数关闭管道的读写端。

注意 :

  • 当管道中没有数据时,读管道会发送阻塞;
  • 当缓冲区的数据长度小于要写入数据的长度时,并且读管道没有及时取走数据时会发送写阻塞;
  • 当管道读端关闭时,写管道会发送管道破裂,内核会给进程发送SIGPIPE
  • 匿名管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道。

命名管道

命名管道通常被称为FIFO。它作为特殊的设备文件存在于文件系统中。因此,在进程中可以使用open()和close()函数打开和关闭命名管道。

  1. 创建一个命名管道
-> mkfifo filename	#创建一个命名管道
#include 
#include 

int mkfifo(const char* pathname, mode_t mode);	//创建命名管道
{
 	@msg
        *pathname :路径及文件名
    	mode :文件的权限(mode&~umask)
    @return
        int :成功返回0,失败返回-1,并设置errorno。 
}
  1. 通过文件IO的系统调用打开/关闭,读/写命名管道。(不支持lseek()接口)

2.1.10.2 信号通信

内核进程向用户进程的通信

​ 信号,是Linux中向进程发送的消息,接收到该信号的进程会相应地采取一些行动,即通过软中断的方式来响应这个信号,触发一些事先指定或特定的事件。

信号是一种异步通信方式,信号是在软件层次上对中断机制的一种模拟。同时,信号是进程间通信机制中唯一的异步通信机制。

用户进程对信号的响应方式

  • 忽略信号:对信号不做任何处理,但是有两个信号不能忽略:即SIGKILL及SIGSTOP。
  • 捕捉信号:定义信号处理函数,当信号发生时,执行相应的处理函数。
  • 执行缺省操作:Linux对每种信号都规定了默认操作

kill -9 无法杀掉的进程

1 用户授权 UNIX提供了安全机制,以防止未授权用户杀死其他进程。实际上,若进程欲向另一个进程发送信号,发送信号的进程的所有者必须与接收信号的进程的所有者相同,或者发送信号的进程的所有者是超级用户root。
2 超级进程 即使root用户也无法向PID为1的进程发送信号。这个进程是系统的第一个进程,PID为1,又叫超级进程,也叫根进程。它负责产生其他所有用户进程。所有的进程都会被挂在这个进程下,如果这个进程退出了,那么所有的进程都被kill。如果一个子进程的父进程退了,那么这个子进程会被挂到PID 1下面,即PPID为1。
3 内核态进程 当一个进程执行系统调用而陷入内核代码中执行时,该进程由用户态转为内核态,处于内核态的进程将忽略所有信号处理。如果进程在执行系统调用时无限期地阻塞,则可能无法终止该进程。
4 僵尸进程 进程停止后,该进程就会从进程列表中移除。但是,有时候有些进程即使执行完了也依然留在进程列表中。这些完成了生命周期但却依然留在进程列表中的进程,我们称之为 “僵尸进程”。

清理僵尸进程

#先尝试通过SIGTERM信号、SIGKILL信号、SIGHUP信号来尝试kill僵尸进程。
	kill PID
	kill -9 PID
	kill -HUP PID
#若无法清理则采用以下方式
    1.查找僵尸进程
        ps aux | egrep "Z|defunct"
    2.获取父进程id
        ps -o ppid= <Child PID>
    3.命令其父回收子进程的资源
        kill -s SIGCHLD <Parernt PID>
    4.父进程无法清理子进程,直接杀父(将此进程变成孤儿进程,交给init进程管理,由init进程回收此进程的资源)
        kill -9 <Parernt PID>		#kill -9 来强制终止退出

发送信号的函数

#include 
#include 

int kill(pid_t pid, int sig);	//向进程发送信号
{
    @msg
        pid :
    		1. pid大于零时,pid是信号欲送往的进程的标识。
            2. pid等于零时,信号将送往所有与调用kill()的那个进程属同一个使用组的进程。
            3. pid等于-1时,信号将送往所有调用进程有权给其发送信号的进程,除了进程1(init)4. pid小于-1时,信号将送往以-pid为组标识的进程。
    	sig :准备发送的信号代码,假如其值为零则没有任何信号送出,但是系统会执行错误检查,通常会利用sig值为零来检验某个进程是否仍在执行。
    @return
        int : 成功执行时,返回0。失败返回-1,errno
}
#include 

int raise(int sig);		//向自己发送信号
{
    @msg
    	sig :准备发送的信号代码。
    @return
        int : 成功执行时,返回0。失败返回-1,errno
}

信号宏和描述

信号代号 信号宏 描述
1 SIGHUP 本信号在用户终端结束时发出,通常是在终端的控制进程结束时,通知同一会话期内的各个作业,这时他们与控制终端不在关联。比如,登录linux时,系统会自动分配给登录用户一个控制终端,在这个终端运行的所有程序,包括前台和后台进程组,一般都属于同一个会话。当用户退出时,所有进程组都将收到该信号,这个信号的默认操作是终止进程。此外对于与终端脱离关系的守护进程,这个信号用于通知它重新读取配置文件。
2 SIGINT 程序终止信号。当用户按下CRTL+C时通知前台进程组终止进程。
3 SIGQUIT Ctrl+\控制,进程收到该信号退出时会产生core文件,类似于程序错误信号。
4 SIGILL 执行了非法指令。通常是因为可执行文件本身出现错误,或者数据段、堆栈溢出时也有可能产生这个信号。
5 SIGTRAP 由断点指令或其他陷进指令产生,由调试器使用。
6 SIGABRT 调用abort函数产生,将会使程序非正常结束。
7 SIGBUS 非法地址。包括内存地址对齐出错。比如访问一个4个字长的整数,但其地址不是4的倍数。它与SIGSEGV的区别在于后者是由于对合法地址的非法访问触发。
8 SIGFPE 发生致命的算术运算错误。
9 SIGKILL 用来立即结束程序的运行。
10 SIGUSR1 留给用户使用,用户可自定义。
11 SIGSEGV 访问未分配给用户的内存区。或操作没有权限的区域。
12 SIGUSR2 留给用户使用,用户可自定义。
13 SIGPIPE 管道破裂信号。当对一个读进程已经运行结束的管道执行写操作时产生。
14 SIGALRM 时钟定时信号。由alarm函数设定的时间终止时产生。
15 SIGTERM 程序结束信号。shell使用kill产生该信号,当结束不了该进程,尝试使用SIGKILL信号。
16 SIGSTKFLT 堆栈错误。
17 SIGCHLD 子进程结束,父进程会收到。如果子进程结束时父进程不等待或不处理该信号,子进程会变成僵尸进程。
18 SIGCONT 让一个停止的进程继续执行。
19 SIGSTOP 停止进程执行。暂停执行。
20 SIGTSTP 停止运行,可以被忽略。Ctrl+z。
21 SIGTTIN 当后台进程需要从终端接收数据时,所有进程会收到该信号,暂停执行。
22 SIGTTOU 与SIGTTIN类似,但在写终端时产生。
23 SIGURG 套接字上出现紧急情况时产生。
24 SIGXCPU 超过CPU时间资源限制时产生的信号。
25 SIGXFSZ 当进程企图扩大文件以至于超过文件大小资源限制时产生。
26 SIGVTALRM 虚拟使用信号。计算的是进程占用CPU调用的时间。
27 SIGPROF 包括进程使用CPU的时间以及系统调用的时间。
28 SIGWINCH 窗口大小改变时。
29 SIGIO 文件描述符准备就绪,表示可以进行输入输出操作。
30 SIGPWR 电源失效信号。
31 SIGSYS 非法的系统调用。

有关函数

alarm()		//设置信号SIGALRM在经过参数seconds秒数后发送给目前的进程
    
#include 
//若在同已进程中设置多个闹钟,后面定时器的设置将覆盖前面的设置
unsigned int alarm(unsigned int seconds);	
{
    @msg
        seconds : seconds为0,则之前设置的闹钟会被取消,seconds>0 表示在seconds秒后使进程终止。
    @return
        unsigned int :返回之前闹钟的剩余秒数,如果之前未设闹钟则返回0}
pause()	//使进程挂起
#include 
int pause(void);
signal()
    
#include 
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);	//注册信号的处理函数
{
	@msg
        signum :指明了所要处理的信号类型
        handler :描述了与信号关联的动作
            SIG_IGN :忽略该信号
            SIG_DFL :默认的处理方式
            函数指针 :当接收到一个类型为sig的信号时,就执行handler 所指定的函数
    @return
        sighandler_t :返回先前的信号处理函数指针,如果有错误则返回SIG_ERR(-1)}


2.1.10.3 System V IPC

System V是Unix操作系统最早的商业发行版之一。

​ 在IPC的通信模式下,不管是使用消息队列还是共享内存,甚至是信号量,每个IPC的对象(object)都有唯一的名字,称为“键”(key)。通过“键”,进程能够识别所用的对象。“键”与IPC对象的关系就如同文件名称之于文件,通过文件名,进程能够读写文件内的数据,甚至多个进程能够共用一个文件。而在IPC的通讯模式下,通过“键”的使用也使得一个IPC对象能为多个进程所共用。

​ 通常,都希望自己的程序能和其他的程序预先约定一个唯一的键值,但实际上并不是总可能的成行的,因为自己的程序无法为一块共享内存选择一个键值。因此,在此把key设为IPC_PRIVATE,这样,操作系统将忽略键,建立一个新的共享内存,指定一个键值,然后返回这块共享内存IPC标识符ID。而将这个新的共享内存的标识符ID告诉其他进程可以在建立共享内存后通过派生子进程,或写入文件或管道来实现。

从文件获取键值

#include 
#include 
key_t ftok ( char *pathname, char proj ); /* f是file,k是key */
{
    @msg
        pathname:文件名;
        proj:项目名,不为0即可
    @return
        key_t:成功返回键值;失败返回-1,并产生错误号。
}

2.1.10.3.1 System V 共享内存

​ OS在物理内存当中申请一块空间,这部分空间称为共享内存(其实就是一段内存空间)。OS把申请好的共享内存通过页表映射到进程A和进程B的共享区中(堆、栈之间)。

​ 这样这两个进程就可以使用各自的虚拟地址以及它自己的页表映射到同一块物理内存,这样两个不同的进程就看到了同一份资源。这就是共享内存的原理。

共享内存的建立有三个核心步骤
1.申请共享内存
2.不同的进程分别挂接对应的共享内存到自己的地址空间(共享区)。
3.双方就看到了同一份资源。即可以进行正常通信了。

  1. 申请共享内存
shmget()	//得到一个共享内存标识符或创建一个共享内存对象并返回共享内存标识符
#include 
#include 

int shmget(key_t key, size_t size, int shmflg);
{
    @msg
        key :标识共享内存的键值       	
			0(IPC_PRIVATE) :会建立新共享内存对象
            大于032位整数 :通常要求此值来源于 ftok() 返回的IPC键值
    	size :要建立共享内存的长度。(仅获取则为0)
    	shmflg :和一些标志有关。其中有效的包括 IPC_CREAT 和 IPC_EXCL,它们的 open() 的 O_CREAT 和O_EXCL相当。
    @return
        int :成功返回共享内存的标识符;不成功返回-1,errno储存错误原因。
}
eg : if(-1 == shmget(IPC_PRIVATE, 64, IPC_CREAT | 0777));	//


//shmid_ds数据结构表示每个新建的共享内存。
//当shmget()创建了一块新的共享内存后,返回一个可以用于引用该共享内存的shmid_ds数据结构的标识符。
struct shmid_ds
{
	struct ipc_perm shm_perm;	 /* operation perms */
	int shm_segsz;				 /* size of segment (bytes) */
	__kernel_time_t shm_atime;	 /* last attach time */
	__kernel_time_t shm_dtime;	 /* last detach time */
	__kernel_time_t shm_ctime;	 /* last change time */
	__kernel_ipc_pid_t shm_cpid; /* pid of creator */
	__kernel_ipc_pid_t shm_lpid; /* pid of last operator */
	unsigned short shm_nattch;	 /* no. of current attaches */
	unsigned short shm_unused;	 /* compatibility */
	void *shm_unused2;			 /* ditto - used by DIPC */
	void *shm_unused3;			 /* unused */
};
  1. 映射地址空间
shmat()		//把共享内存区对象映射到调用进程的地址空间

#include 
#include 
void *shmat(int shmid, const void *shmaddr, int shmflg);
{
	@msg
        shmid: 共享内存标识符
        *shmaddr: 指定共享内存出现在进程内存地址的什么位置;若为NULL,则让其自己决定
    	shmflg: SHM_RDONLY:为只读模式,其他为读写模式
    @return
        void *:成功:附加好的共享内存地址;出错:-1,错误原因存于error中
}
eg: char *p = (char *)shmat(shmid, NULL, 0);

注意

  • fork后子进程继承已连接的共享内存地址。
  • exec后该子进程与已连接的共享内存地址自动脱离(detach)。
  • 进程结束后,已连接的共享内存地址会自动脱离(detach)。
  1. 销毁映射
shmdt()		//销毁
#include 
#include 
int shmdt(const void *shmaddr);
{
    @msg
        *shmaddr: 连接的共享内存的起始地址
    @return
        int: 成功:0;出错:-1,错误原因存于error中
}
  1. 删除共享内存
#include 
#include 

int shmctl(int shmid, int cmd, struct shmid_ds *buf);
{
 	@msg
        msqid :共享内存标识符
		cmd :
            IPC_STAT:得到共享内存的状态,把共享内存的shmid_ds结构复制到buf中
            IPC_SET:改变共享内存的状态,把buf所指的shmid_ds结构中的uid、gid、mode复制到共享内存的shmid_ds结构内
            IPC_RMID :删除这片共享内存
		buf :共享内存管理结构体。
    @return
        int : 成功:0;出错:-1,错误原因存于error中
}

2.1.10.3.2 System V 消息队列

消息队列就是一个存储消息的链表,这些消息具有特定的格式,以及特定的优先级。
对消息队列有写权限的进程可以向其中添加新消息,对消息队列有读权限的进程则可以从其中读取消息。
消息队列是随内核持续的,只有在内核重启,或者删除一个消息队列时,该消息队列才会真正地被删除。

在消息队列中,每一个消息的格式struct msgbuf

消息类型必须是长整型的,而且必须是结构体类型的第一个成员,类型下面是消息正文,正文可以有多个成员(正文成员可以是任意数据类型的)。

struct msgbuf {
    long mtype;    		/*消息类型*/
    char mtext[100]; 	/*消息正文*/
    ... /*消息的正文可以有多个成员*/
};

相关shell命令

ipcs -q			#查看消息队列    
ipcrm -qmsqid	#删除消息队列    
  1. 创建消息队列
#include 
#include 
#include 
int msgget ( key_t key, int msgflg );
{
 	@msg
        key是键值,由ftok获得
		msgflg是标志位,有如下选项,这些选项可以进行或操作:
        	1.IPC_CREAT:创建新的消息队列。
            2.IPC_EXCL:与IPC_CREAT一同使用,表示如果要创建的消息队列已经存在,则返回错误。
            3.IPC_NOWAIT:当消息队列无法满足读写需求时,msgsnd与msgrcv不被阻塞。
    @return
        int :成功:消息队列的标识符;失败:返回-1 并设置errorno。
}
  1. 向消息队列发送一条消息
#include 
#include 
#include 
int msgsnd ( int msqid, struct msgbuf *msgp, int msgsz, int msgflg );
{
	@msg
        msqid:已打开的消息队列id。
        *msgp:指向存放消息的结构体的指针。
        msgsz:消息数据长度。
        msgflg:发送标志,常用0和IPC_NOWAIT。
    @return
        int :成功:0;失败:返回-1 并设置errorno。
}

msgflg有两种:

  • 如果 msgflg = 0,则当消息队列没有足够空间容纳要发送的消息时,msgsnd将被阻塞
  • 如果 msgflg = IPC_NOWAIT,则当消息队列没有足够空间容纳要发送的消息时,msgsnd不会被阻塞
  1. 从消息队列msqid中读取一个msgtyp类型的消息
#include 
#include 
#include 

//在成功地读取了一条消息以后,消息队列中的这条消息将被删除。
int msgrcv ( int msqid, struct msgbuf *msgp, int msgsz, long msgtyp, int msgflg );
{
    @msg
        msqid:已打开的消息队列id。
        *msgp:存放消息的结构。
        msgsz:消息数据长度。
        msgtyp:msgtype可以实现一种简单的接收优先级。   
        msgflg:发送标志,常用0和IPC_NOWAIT。
    @return
        int :成功返回 读取消息的长度;失败返回 -1 并设置errorno。
}

msgtype可以实现一种简单的接收优先级(通过消息类型)

  1. 如果msgtype0,就获取队列中的第一个消息
  2. 如果msgtype大于零,就获取具有相同消息类型的第一个消息
  3. 如果msgtype小于零,就获取类型等于或小于msgtype的绝对值的第一个消息

msgflg有如下选项,这些选项可以进行操作:

  • 0忽略msgflg
  • IPC_NOWAIT:如果没有满足条件的消息,则函数立即返回。
  • IPC_EXCEPT:与msgtyp > 0配合使用,返回消息队列中**第一个类型不为msgtyp**的消息。
  • IPC_NOERROR:如果消息队列中满足条件的消息内容大于所请求的msgsz字节,则把该消息截断,截断部分将丢失。
  1. 对消息队列msqid执行cmd操作
int msgctl ( int msqid, int cmd, struct msqid_ds *buf );
{
 	@msg
        msqid :已打开的消息队列id
    	cmd :函数功能的控制
    	*buf :msqid_ds数据类型的地址,用来存放或更改消息队列的属性。
    @return
        int :成功:返回0;失败:返回 -1 并设置errorno
}

共有3cmd操作:

  • IPC_STAT:用来获取消息队列的属性,返回的属性存储在buf指向的msqid结构中。
  • IPC_SET:用来设置消息队列的属性,要设置的属性存储在buf指向的msqid结构中。
  • IPC_RMID删除的消息队列msqid

2.1.10.3.3 System V 信号量

system v 信号量 又被称为system v信号量集,它的本质是是一种数据操作锁(相当于资源计数器),用来同步、协调多个进程之间的数据交换,而自身不具备数据交换功能,信号量是由于同步与互斥的高级管理而提出来的,它支持两种原子性操作,wait(p)与signal(v),其作用是原子性的增加或减少信号量的值 ,主要的作⽤是维护资源的互斥或多进程的同步访问。

System V 信号量是为了防止多个进程同时访问临界资源所引发的一系列问题(说法有一些欠缺),因为信号量的存在,多个进程在同一时间只能有一个进程去访问临界资源,以协调对临界资源的访问。

互斥量与信号量

  • 互斥量:用来保护临界区的 ,所谓临界区,是指同一时间只能容许一个进程进入。与二元信号量一样
  • 信号量:是用来管理资源的,资源的个数不一定是一个,可能同时存在多个一模一样的资源,因此容许多个进程同时使用资源。
  1. 创建信号量
semget()
    
#include 
#include 
#include 
int semget(key_t key, int nsems, int semflg);
{
	@msg
        key :key是整数值,不相关的进程可以通过它访问同一信号量。
    	nsems :指定需要的信号量数目
    	semflg :是一组标志,与open函数的标志非常相似。
    @return
        int :成功时返回一个信号量标识符(大于0的值),错误返回-1,并设置errno。
}
  1. P/V操作
#include 
#include 
#include 

int semop(int semid, struct sembuf *sops, size_t nsops);
{
	@msg
        semid:信号量标识符
        *sops:指向一个结构数组的指针
        nsops:操作结构的数量
     @return
        int :成功返回0。错误返回-1,errno表示错误
}

struct sembuf{
    short sem_num;	//是一个信号量编号,除非需要使用一组信号量,否则它的取值一般为0。
    short sem_op;	//是信号量在一次操作中需要改变的数值。	(-1 : P操作) (+1 : V操作)
    short sem_flg;	//通常被设置为SEM_UNDO。它将使得操作系统跟踪当前进程对这个信号量的修改情况。如果这个进程在没有释放该信号量的情况下终止,操作系统将自动释放该进程持有的信号量。
}

  1. 控制信号量信息
#include 
#include 
#include 

int semctl(int semid, int semnum, int cmd, ...);
{
    @msg
        sem_id : 由semget返回的信号量标识符。
        sem_num : 是信号量编号,当需要用到成组的信号量时,就要用到这个参数,它一般取值为0,表示这是第一个也是唯一的一个信号量。
        cmd :是将要采取的动作。
        ... :在修改/读取信号量的信息时,会有第四个参数,它将会是一个union semun 共用体结构。
    @return
        int :根据command参数的不同而返回不同的值
}

union semun{
    int val;
    struct semid_ds *buf;
    unsigned short *array;
}

comman参数可以设置许多不同的值,但只有下面介绍的两个值最常用。

  • SETVAL:用来把信号量初始化为一个已知的值。这个值通过union semun中的val成员设置。其作用是在信号量第一次使用之前对它进行设置。
  • IPC_RMID:用于删除一个已经无需继续使用的信号量标识符。

对于SETVAL和IPC_RMID,成功时返回0,失败时返回-1.


2)线程

  1. 概念

    共享同一个进程地址空间的多个任务叫线程。

  2. 特点

    • 同一个进程中的多个线程共享进程的地址空间

    • 轻量级的进程

    • 线程统一参与CPU的调用,即线程间的执行顺序不确定

  3. 共享进程的资源

    • 可执行的指令

    • 静态数据(例如:全局变量)

    • 进程打开的文件描述符

    • 信号处理函数(signal)

    • 工作目录

    • 用户ID

    • 用户组ID

  4. 线程私有的资源

    • 线程ID (TID)

    • PC(程序计数器)和相关寄存器

    • 堆栈

      • 局部变量、函数的形参
      • 函数的返回地址
    • 错误号 (errno)

    • 信号掩码和优先级

    • 执行状态和属性


线程的意义:

  1. 实现真正的并发;
  2. 有一些情况下会产生阻塞,我们可以将阻塞放到某个线程中,保证我们程序整体可以继续向下执行;
  3. 对于一些计算密集的任务,我们可以专门开启一个线程去执行。

三、设置线程的优先级
Linux内核的三种调度策略:

  1. SCHED_OTHER 分时调度策略: 它是默认的线程分时调度策略,所有的线程的优先级别都是0,线程的调度是通过分时来完成的。简单地说,如果系统使用这种调度策略,程序将无法设置线程的优先级。请注意,这种调度策略也是抢占式的,当高优先级的线程准备运行的时候,当前线程将被抢占并进入等待队列。这种调度策略仅仅决定线程在可运行线程队列中的具有相同优先级的线程的运行次序。
  2. SCHED_FIFO实时调度策略,先到先服务。 它是一种实时的先进先出调用策略,且只能在超级用户下运行。这种调用策略仅仅被使用于优先级大于0的线程。它意味着,使用SCHED_FIFO的可运行线程将一直抢占使用SCHED_OTHER的运行线程。此外SCHED_FIFO是一个非分时的简单调度策略,当一个线程变成可运行状态,它将被追加到对应优先级队列的尾部((POSIX 1003.1)。当所有高优先级的线程终止或者阻塞时,它将被运行。对于相同优先级别的线程,按照简单的先进先运行的规则运行。我们考虑一种很坏的情况,如果有若干相同优先级的线程等待执行,然而最早执行的线程无终止或者阻塞动作,那么其他线程是无法执行的,除非当前线程调用如pthread_yield之类的函数,所以在使用SCHED_FIFO的时候要小心处理相同级别线程的动作。
  3. SCHED_RR实时调度策略,时间片轮转。当进程的时间片用完,系统将重新分配时间片,并置于就绪队列尾。放在队列尾保证了所有具有相同优先级的RR任务的调度公平。
在任何一个时间点上,线程是可结合的(joinable),或者是分离的(detached)。

1. 一个可结合的线程能够被其他线程收回其资源和杀死;在被其他线程回收之前,它的存储器资源(如栈)是不释放的。
	
2. 相反,一个分离的线程是不能被其他线程回收或杀死的,它的存储器资源在它终止时由系统自动释放。



1.  线程的分离状态决定一个线程以什么样的方式来终止自己。
    在默认情况下线程是非分离状态的,这种情况下,原有的线程等待创建的线程结束。
    只有当pthread_join()函数返回时,创建的线程才算终止,才能释放自己占用的系统资源。

2.  而分离线程不是这样子的,它没有被其他的线程所等待,自己运行结束了,线程也就终止了,马上释放系统资源。
    程序员应该根据自己的需要,选择适当的分离状态。
    所以如果我们在创建线程时就知道不需要了解线程的终止状态,
    则可以pthread_attr_t结构中的detachstate线程属性,让线程以分离状态启动。

线程对资源的竞争范围

  1. 线程资源的竞争范围(PTHREAD_SCOPE_SYSTEM 或 PTHREAD_SCOPE_PROCESS);
  2. 使用 PTHREAD_SCOPE_SYSTEM 时,此线程将与系统中的所有线程进行竞争。使用 THREAD_SCOPE_PROCESS 时,此线程将与进程中的其他线程进行竞争。

2.2.1 创建线程

pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了,栈空间就会被回收。

pthread_create()	//创建子线程

#include 
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, 
                   void *(*start_routine) (void *), void *arg); 
{
    @msg
		*thread :指向保存线程tid的变量的指针
		*attr :线程属性指针 NULL:默认属性
        *start_routine(*) :指向线程处理函数
		*arg :传递给线程处理函数形参的参数值
	@return
        int : 成功:0;失败: 返回errno;
}
2.2.2 结束单个线程
pthread_exit()	//将单个线程退出

#include 
void pthread_exit(void *retval);	
{
	@msg
        *retval :表示线程退出状态,通常传 NULL(指针所指向的内存单元必须是全局的或者是用malloc分配)
}
2.2.3 回收某个线程
pthread_join()	//阻塞等子线程结束

#include 
int pthread_join(pthread_t thread, void **retval);	
{
	@msg
		thread :需要等待的线程,指定的线程必须位于当前的进程中,而且不得是分离线程
		**retval :指向指定线程的退出状态的指针(用来存储被等待线程的返回值)。
	@return
        int :成功: 0;失败: 返回errno;
}
pthread_detach()	//非阻塞等子线程结束,子线程结束后,资源自动回收。
    
#include 
int pthread_detach(pthread_t thread);	
{
    @msg
        thread :线程标识符
    @return
        int :成功: 0;失败: 返回errno;
}

2.2.4 线程间的同步和互斥
  1. 线程间通信
    使用全局变量
  2. 同步
    解决:全局变量的先后访问顺序问题
    概念:多个线程按照事先约定好的先后顺序访问资源
    实现:信号量
  3. 互斥
    解决:临界资源不被破坏
    概念:同一时刻只能有一个线程访问资源
    实现互斥:互斥锁

2.2.5 线程间的通信

线程是操作系统调度的最小单位,有自己的栈空间,可以按照既定的代码逐步的执行,但是如果每个线程间都孤立的运行,那就会造资源浪费。

​ 所以在现实中,我们需要这些线程间可以按照指定的规则共同完成一件任务,所以这些线程之间就需要互相协调,这个过程被称为线程的通信

线程通信就是当多个线程共同操作共享的资源时,互相告知自己的状态以避免资源争夺。

2.2.5.1 信号量

解决线程间的读写数据的先后问题(实现同步)

  • 信号量代表某一类资源,其值表示系统中该资源的数量

  • 信号量是一个受保护的变量,只能通过三种操作来访问

    • sem_init() //初始化
    • sem_wait() //P操作(申请资源)
    • sem_post() //V操作(释放资源)
  • 信号量的值为非负整数

p/v操作的理解

P(S) 含义如下:

if  (信号量的值大于0) 
{
	// 申请资源的任务继续运行;
	// 信号量的值减一;
}
else 
{   
    // 申请资源的任务阻塞;
}

V(S) 含义如下:

if  (没有任务在等待该资源) 
{
    // 信号量的值加一;
}
else 
{   
    // 唤醒第一个等待的任务,让其继续运行
}

信号量通信函数

sem_init()	//初始化信号量

#include 
int sem_init(sem_t *sem, int pshared, unsigned int value);
{
    @msg
		*sem :指向信号量的指针
		pshared:
			0:线程间的同步
			非0:进程间的同步
		value :信号量的初值
    @return
        int :成功:0;失败:返回-1 并且设置errno的值;
}
sem_wait()	//执行p操作,即申请资源

#include 
int sem_wait(sem_t *sem);
{
    @msg
		*sem :指向信号量的指针
	@return
		int :成功:0; 失败:返回-1 并且设置errno的值	
}
sem_post()	//执行v操作,即释放资源

#include 
int sem_post(sem_t *sem);
{
    @msg
		*sem :指向信号量的指针
	@return
		int :成功:0; 失败:返回-1 并且设置errno的值
}
2.2.5.2 互斥锁

保护临界资源在多个写入时不被破环(实现互斥)

  1. 创建锁资源

    pthread_mutex_t mutex

    因为线程是共享数据区和堆区的,所以我们可以创建全局或静态的锁变量或者在堆上创建,这样就可以使得所有线程都可以看见锁,所以说实现线程间通信是非常简单的。

  2. 初始化锁

    • pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER
    • pthread_mutex_init(&mutex, NULL)

    如果创建的是全局或静态的锁的话,可以用宏PTHREAD_MUTEX_INITIALIZER初始化。也可以用函数初始化。
    attr:变量表示锁的属性。为NULL的话就相当于用宏初始化。
    返回值:成功返回0,失败返回错误码。

   #include 
   int pthread_mutex_init(pthread_mutex_t* mutex, const pthread_mutexattr_t* attr);
   {
       @msg
           *mutex :锁的地址
       	*attr :锁的属性
   	@return
           int :成功返回0,失败返回错误码。
   }

锁的属性

  • PTHREAD_MUTEX_TIMED_NP,这是缺省值,也就是普通锁。当一个线程加锁以后,其余请求锁的线程将形成一个等待队列,并在解锁后按优先级获得锁。这种锁策略保证了资源分配的公平性。

  • PTHREAD_MUTEX_RECURSIVE_NP嵌套锁,允许同一个线程对同一个锁成功获得多次,并通过多次unlock解锁。如果是不同线程请求,则在加锁线程解锁时重新竞争。

  • PTHREAD_MUTEX_ERRORCHECK_NP检错锁,如果同一个线程请求同一个锁,则返回EDEADLK,否则与PTHREAD_MUTEX_TIMED_NP类型动作相同。这样保证当不允许多次加锁时不出现最简单情况下的死锁。

  • PTHREAD_MUTEX_ADAPTIVE_NP适应锁,动作最简单的锁类型,仅等待解锁后重新竞争。

  1. 加锁

    int pthread_mutex_lock(pthread_mutex_t* mutex);

  2. 解锁

    int pthread_mutex_unlock(pthread_mutex_t *mutex);

  3. 销毁锁

    int pthread_mutex_destroy(pthread_mutex_t* mutex);

    注意:

    • 使用PTHREAD_ MUTEX_ INITIALIZER初始化的互斥量不需要销毁
    • 不要销毁一个已经加锁的互斥量
    • 已经销毁的互斥量,要确保后面不会有线程再尝试加锁

死锁

死锁危害:线程一直等待,可能导致整个服务器崩溃
死锁的两个常见场景:

  • 一个线程获取到锁之后,又尝试获取锁,就会出现死锁
  • 两个线程A和B,线程A获取了锁1,线程B获取了锁2。然后A尝试获取锁2,B尝试获取锁1。这个时候双方都无法拿到对方的锁,并且会在获取锁的函数中阻塞等待
    如果线程数和锁的数目更多了,就会使死锁问题更容易出现,问题场景更复杂

对于这种需要获取多个锁的场景,规定所有的线程都按照固定的顺序来获取锁,能够一定程度上避免死锁。

避免死锁,总体来讲,有几个不成文的基本原则:

  • 对共享资源操作前一定要获得锁。
  • 完成操作以后一定要释放锁。
  • 尽量短时间地占用锁。
  • 如果有多锁, 如获得顺序是ABC连环扣, 释放顺序也应该是ABC。
  • 线程错误返回时应该释放它所获得的锁。
sem_wait()	//执行p操作,即申请资源

#include 
int sem_wait(sem_t *sem);
{
    @msg
		*sem :指向信号量的指针
	@return
		int :成功:0; 失败:返回-1 并且设置errno的值	
}
sem_post()	//执行v操作,即释放资源

#include 
int sem_post(sem_t *sem);
{
    @msg
		*sem :指向信号量的指针
	@return
		int :成功:0; 失败:返回-1 并且设置errno的值
}
2.2.5.2 互斥锁

保护临界资源在多个写入时不被破环(实现互斥)

  1. 创建锁资源

    pthread_mutex_t mutex

    因为线程是共享数据区和堆区的,所以我们可以创建全局或静态的锁变量或者在堆上创建,这样就可以使得所有线程都可以看见锁,所以说实现线程间通信是非常简单的。

  2. 初始化锁

    • pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER
    • pthread_mutex_init(&mutex, NULL)

    如果创建的是全局或静态的锁的话,可以用宏PTHREAD_MUTEX_INITIALIZER初始化。也可以用函数初始化。
    attr:变量表示锁的属性。为NULL的话就相当于用宏初始化。
    返回值:成功返回0,失败返回错误码。

   #include 
   int pthread_mutex_init(pthread_mutex_t* mutex, const pthread_mutexattr_t* attr);
   {
       @msg
           *mutex :锁的地址
       	*attr :锁的属性
   	@return
           int :成功返回0,失败返回错误码。
   }
   

锁的属性

  • PTHREAD_MUTEX_TIMED_NP,这是缺省值,也就是普通锁。当一个线程加锁以后,其余请求锁的线程将形成一个等待队列,并在解锁后按优先级获得锁。这种锁策略保证了资源分配的公平性。

  • PTHREAD_MUTEX_RECURSIVE_NP嵌套锁,允许同一个线程对同一个锁成功获得多次,并通过多次unlock解锁。如果是不同线程请求,则在加锁线程解锁时重新竞争。

  • PTHREAD_MUTEX_ERRORCHECK_NP检错锁,如果同一个线程请求同一个锁,则返回EDEADLK,否则与PTHREAD_MUTEX_TIMED_NP类型动作相同。这样保证当不允许多次加锁时不出现最简单情况下的死锁。

  • PTHREAD_MUTEX_ADAPTIVE_NP适应锁,动作最简单的锁类型,仅等待解锁后重新竞争。

  1. 加锁

    int pthread_mutex_lock(pthread_mutex_t* mutex);

  2. 解锁

    int pthread_mutex_unlock(pthread_mutex_t *mutex);

  3. 销毁锁

    int pthread_mutex_destroy(pthread_mutex_t* mutex);

    注意:

    • 使用PTHREAD_ MUTEX_ INITIALIZER初始化的互斥量不需要销毁
    • 不要销毁一个已经加锁的互斥量
    • 已经销毁的互斥量,要确保后面不会有线程再尝试加锁

死锁

死锁危害:线程一直等待,可能导致整个服务器崩溃
死锁的两个常见场景:

  • 一个线程获取到锁之后,又尝试获取锁,就会出现死锁
  • 两个线程A和B,线程A获取了锁1,线程B获取了锁2。然后A尝试获取锁2,B尝试获取锁1。这个时候双方都无法拿到对方的锁,并且会在获取锁的函数中阻塞等待
    如果线程数和锁的数目更多了,就会使死锁问题更容易出现,问题场景更复杂

对于这种需要获取多个锁的场景,规定所有的线程都按照固定的顺序来获取锁,能够一定程度上避免死锁。

避免死锁,总体来讲,有几个不成文的基本原则:

  • 对共享资源操作前一定要获得锁。
  • 完成操作以后一定要释放锁。
  • 尽量短时间地占用锁。
  • 如果有多锁, 如获得顺序是ABC连环扣, 释放顺序也应该是ABC。
  • 线程错误返回时应该释放它所获得的锁。

附录:进程内存空间图

Linux的IO和进程、线程_第2张图片

文章目录

  • IO进线程
    • 一、IO
      • 1)标准IO(C库提供)
        • 1.1 前言
        • 1.2 IO函数
          • 1.2.1 打开文件流`fopen()`
          • 1.2.2 关闭文件流`fclose()`
          • 1.2.3 读取文件
          • 1.2.4 文件流指针
          • 1.2.5 错误处理
      • 2)文件IO
        • 1.2.1 文件接口
          • 1.2.1.1 打开文件`open()`
          • 2.1.2 关闭文件`close()`
          • 1.2.1.3 读取内容`read()`
          • 1.2.1.4 写入内容`write()`
          • 1.2.1.5 文件定位`lseek()`
        • 1.2.2 目录接口
          • 1.2.2.1 目录的打开和关闭
          • 1.2.2.2 目录的读取
          • 1.2.2.3 文件的详细信息
          • 1.2.2.4 文件的用户名
          • 1.2.2.5 文件的组名
          • 1.2.2.6 获取链接文件的目标文件
          • 1.2.2.7 修改文件权限
      • 3)库
        • 1.3.0 静态库和动态库
        • 1.3.1 建立库
        • 1.3.2 使用库
        • 1.3.3 添加动态库路径
        • 1.3.4 gcc命令
    • 二、进线程
      • 1)进程
        • 2.1.1 进程控制块
        • 2.1.2 进程类型
        • 2.1.3 进程的状态
        • 2.1.4 进程相关命令
        • 2.1.5 创建进程
        • 2.1.6 结束进程
        • 2.1.7 回收进程
        • 2.1.8 exec函数族
        • 2.1.9 创建守护进程
        • 2.1.10 进程间的通信
          • 2.1.10.1 管道通信
          • 2.1.10.2 信号通信
          • 2.1.10.3 System V IPC
            • 2.1.10.3.1 System V 共享内存
            • 2.1.10.3.2 System V 消息队列
            • 2.1.10.3.3 System V 信号量
      • 2)线程
        • 2.2.1 创建线程
        • 2.2.2 结束单个线程
        • 2.2.3 回收某个线程
        • 2.2.4 线程间的同步和互斥
        • 2.2.5 线程间的通信
          • 2.2.5.1 信号量
          • 2.2.5.2 互斥锁
          • 2.2.5.2 互斥锁
    • 附录:进程内存空间图

你可能感兴趣的:(学习笔记,linux)