Linux网络编程笔记——第四章 程序,进程和线程

目录

一,程序,进程和线程的概念

1,进程和程序的区别

2,Linux环境下的进程

3,进程和线程

二,进程产生的方式

1,进程号

2,进程复制fork()

3,system()方式

4,进程执行exec()函数系列

三,进程间通信和同步

1,半双工管道

2,命名管道

3,消息队列

4,消息队列的例子

5,信号量

6,共享内存

7,信号

四,Linux下的线程

1,多线程编程

2,Linux下线程创建函数pthread_create()

3,线程的结束函数pthread_join()和pthread_exit()

4,线程的属性

5,线程间的互斥

6,线程中使用信号量


一,程序,进程和线程的概念

1,进程和程序的区别

1.1 进程和程序概念最大不同之处

  • 进程是动态的吗,而程序是静态的
  • 进程有一定的生命期,而程序是指令的集合,本身无”运动”的含义。没有建立进程的程序不能作为1个独立单位得到操作系统的认可
  • 一个进程只能对应一个程序,一个程序可以对应多个进程

 

2,Linux环境下的进程

Linux的进程操作方式主要有产生进程,终止进程,并且进程之间存在数据和控制的交互,即进程间通信和同步

 

2.1 进程的产生过程

进程的产生有多种方式,其基本过程是一致的。

  1. 首先复制其父进程的环境配置
  2. 在内核建立进程结构
  3. 将结构插入到进程列表,便于维护
  4. 分配资源给此进程
  5. 复制父进程的内存映射信息
  6. 管理文件描述符和链接点
  7. 通知父进程

 

2.2 进程的终止方式

有5种方式使进程终止

  • 从main返回
  • 由一个信号终止
  • 调用abort
  • 调用_exit
  • 调用exit

进程在终止的时候,系统会释放进程所拥有的资源,例如内存,文件符和内核结构

 

2.3 进程之间的通信

进程间通信方式有多种,其中管道,共享内存和消息队列是最常用的方式

 

2.4 进程之间的同步

Linux下进程的同步方式主要有消息队列,信号量等

 

3,进程和线程

1. 进程和线程的主要区别和联系

  •  进程是操作系统进行资源分配的基本单位,进程拥有完整的虚拟空间。进行系统资源分配的时候,除了CPU资源之外,不会给线程分配独立的资源,线程所需要的资源需要共享。
  •  进程有进程控制块表PCB,系统通过PCB对进程进行调度;线程有线程控制块表TCB,但是TCB所表示的状态比PCB要少得多。
  •  多线程和多进程是两种不同的概念,虽然二者都是并行完成功能。但是,多个线程之间像内存,变量等资源可以通过简单的办法共享,多进程则不同,进程间的共享方式有限。
  •  线程是进程的一部分,如果没有进行显式的线程分配,可以认为进程是单线程的;如果进程中建立了线程,则认为系统是多线程的。

 

二,进程产生的方式

1,进程号

每个进程在初始化的时候,系统都分配了一个ID号,用于标识该进程。描述进程的ID号通常叫做PID

 

1.1 getpid(),getppid()函数介绍

pid_t getpid(void) :返回当前进程的ID号

pid_t getppid(void):返回当前进程的父进程的ID号

  • 包含头文件:sys/type.h,unistd.h

 

1.2 getpid()例子:获取当前程序的PID和父进程的PID

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

int main()
{
    pid_t pid, ppid;

    pid = getpid();	//获取当前进程ID    
    ppid = getppid();	//获取父进程ID

    printf("pid = %d\n", pid);
    printf("ppid = %d\n", ppid);

    return 0;
}

 

2,进程复制fork()

        产生进程的方式比较多,fork()是其中的一种方式。fork()函数以父进程为蓝本复制一个进程,其ID号和父进程ID号不同。在Linxu环境下,fork()是以写复制实现的,只有内存等与父进程不同时,其他与父进程共享,只有在父进程或者子进程进行了修改后,才重新生成一份。

 

2.1 fork()函数介绍

pid_t fork(void)

  • 包含头文件:sys/types.h,unistd.h
  • 返回值:
    • 成功时返回值是进程的ID
    • 失败返回-1

fork()的特点是执行一次,返回两次。在父进程和子进程中返回的是不同的值,父进程中返回的是子进程的ID号,子进程中返回0。

 

2.2 fork()函数例子

在调用fork()函数之后,判断fork()函数的返回值:如果为-1,打印失败信息;如果为0,打印子进程信息;如果大于0,打印父进程信息。

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

int main()
{
    pid_t pid;

    pid = fork();	//分叉进程

    if (-1 == pid)
    {
        printf("进程创建失败!\n");
        return -1;
    }
    else if (pid == 0)
    {
        printf("子进程,fork返回值: %d, ID:%d, 父进程ID:%d\n", pid, getpid(), getppid());
    }
    else
    {
        printf("父进程,fork返回值: %d, ID:%d, 父进程ID:%d\n", pid, getpid(), getppid());
    }

    return 0;
}

父进程,fork返回值: 51240, ID:51239, 父进程ID:25423 子进程,fork返回值: 0, ID:51240, 父进程ID:51239

fork出来的子进程的父进程的ID号是执行fork()函数的进程的ID号。

 

3,system()方式

system()函数调用shell的外部命令在当前进程中开始另一个进程

 

3.1 system()函数介绍

#include <stdlib.h>

int system(const char *command);

执行system()函数时,会调用fork(),execve(),waitpid()等函数,其中任意一个调用失败,将导致system()函数调用失败。

system()函数返回值

  • 失败,返回-1
  • 当sh不能执行时,返回127
  • 成功,返回进程状态值

 

4,进程执行exec()函数系列

exec()族函数会用新进程代替原有的进程,系统会从新的进程运行,新进程的PID值会与原来进程的PID值相同。

 

4.1 exec()族函数共有6个

#include <unistd.h>

extern char **environ;

int execl(const char *path, const char *arg, … );

int execlp(const char *file, const char *arg, … );

int execle(const char *path, const char *arg, … , char * const envp[] );

int execv(const char *path, char *const argv[]);

int execvp(const char *file, char *const argv[]);

int execve(const char *file, char *const argv[], char *const envp[]);

上述6个函数中,只有execve()函数是真正意义上的系统调用,其他5个函数都是在此基础上经过包装的库函数。上述的exec()函数族的作用是,在当前系统的可执行路径中根据指定的文件名来找到合适的可执行文件名,并用它来取代调用进程的内容,即在原来的进程内部运行一个可执行文件。上述的可执行文件即可以是二进制的文件,也可以是可执行的脚本文件。

与fork()函数不同,exec()函数族的函数执行成功后不会返回,这是因为执行的新程序已经占用了当前进程的空间和资源,这些资源包括代码段,数据段和堆栈等,它们都已经被新的内容取代,而进程的ID等标识性的信息任然是原来的东西,即exec()函数族在原来进程的壳上运行了自己的程序,只有程序调用失败了,系统才会返回-1.

如果在fork()系统调用之后进行exec()系统调用,系统就不会进行系统复制,而是直接使用exec()指定参数来覆盖原有的进程。上述的方法在Linux系统上叫做“写时复制”,即只有在造成系统的内容发生更改的时候才进行进程的真正更新。

 

4.2 execve()函数的例子

先打印调用进程的进程号,然后调用execve()函数,这个函数调用可执行文件“bin/ls”列出当前目录下的文件

#include <stdio.h>
#include <unistd.h>

#define FILELENGTH 80

int main(void)
{
    char *args[] = {"/bin/ls", NULL};

    printf("系统分配的进程号是%d\n", getpid());
    if (execve("/bin/ls", args, NULL) < 0)
    {
        printf("创建进程出错\n");
    }
    return 0;
}

 

三,进程间通信和同步

1,半双工管道

管道是一种把两个进程之间的标准输入和标准输出连接起来的机制。

 

1.1 基本概念

在shell中管道用”|”表示,ls -l|grep *.c 把ls -l的输出当做grep *.c的输入,管道在前一个进程中建立输入通道,在后一个进 程中建立输出通道。

进程创建管道,每次创建两个文件描述符来操作管道,其中一个对管道进行写操作,另一个对管道进行读操作

 

1.2 pipe()函数介绍

int pipe(int pipefd[2])

  • 包含头文件:unistd.h
  • pipefd[2]:文件描述符的数组,用于保存管道返回的两个文件描述符。数组中的第一个元素(下标为0)是为了读操作而创建打开的,第二个元素(下标为1)是为了写操作创建打开的。
  • 返回值:成功时返回0,失败时返回-1

管道的操作是阻塞的。

 

1.3 pipe()函数例子

在子进程中向管道写入数据,在父进程中读取数据

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main(void)
{
    int result = -1;
    pid_t pid;
    int fd[2];
    int nbyte;

    char string[] = "你好,管道";
    char readbuff[80];
    //文件描述符1用于写,文件描述符0用于读
    int *write_fd = &fd[1];
    int *read_fd = &fd[0];

    result = pipe(fd);
    if (-1 == result)
    {
        printf("建立管道失败\n");
        return -1;
    }

    pid = fork();
    if (-1 == pid)
    {
        printf("fork 进程失败\n");
        return -1;
    }

    if (pid == 0)           //子进程
    {
        close(*read_fd);     //关闭读管道
        result = write(*write_fd, string, strlen(string));
        return 0;
    }
    else                    //父进程
    {
        close(*write_fd);    //关闭写管道
        nbyte = read(*read_fd, readbuff, sizeof(readbuff));

        printf("接收到%d数据,内容为%s\n", nbyte, readbuff);
    }

    return 0;
}

 

2,命名管道

不同的进程可以通过命名管道共享数据

 

2.1 创建FIFO

  • mkfifo namedfifo
  • int mkfifo(const char *pathname, mode_t mode);

2.2 FIFO操作

        对命名管道来说,IO操作与普通的管道IO操作基本上是一样的,二者之间存在着一个主要的区别。在FIFO中,必须使用一个open()函数来显示的建立连接到管道的通道。一般来说FIFO总是处于阻塞状态。也就是说,如果命名管道FIFO打开时设置了读权限,则读进程将一直阻塞,一直到其他进程打开该FIFO并且向管道中写入数据。这个阻塞动作反过来也是成立的,如果一个进程打开一个管道写入数据,当没有进程从管道中读取数据的时候,写管道的操作也是阻塞的,知道已经写入的数据被读出后,才能进行写入操作。如果不希望在进行命名管道操作的时候发生阻塞,可以在open()调用中使用O_NONBLOCK标志,以关闭默认的阻塞操作。

 

3,消息队列

消息队列是内核地址空间中的内部链表,通过Linux内核在各个进程之间传递内容。消息顺序地发送到消息队列中,并以几种不同地方式从队列中获取,每个消息队列可以用IPC标识符唯一地进行标识。

 

3.1 消息缓冲区结构

常用的结构是msgbuf结构

struct msgbuf {
  	long mtype;
    char mtext[1];  
};
  • mtype:消息类型,以正数来表示。用户可以给某个消息设定一个类型,可以在消息队列中正确地发送和接收自己的消息。
  • mtext:消息数据。可以根据实际的情况进行设定。

 

3.2 结构msgid_ds

struct msqid_ds {
	struct ipc_perm msg_perm;
	struct msg *msg_first;		/* first message on queue,unused  */
	struct msg *msg_last;		/* last message in queue,unused */
	__kernel_time_t msg_stime;	/* last msgsnd time */
	__kernel_time_t msg_rtime;	/* last msgrcv time */
	__kernel_time_t msg_ctime;	/* last change time */
	unsigned long  msg_lcbytes;	/* Reuse junk fields for 32 bit */
	unsigned long  msg_lqbytes;	/* ditto */
	unsigned short msg_cbytes;	/* current number of bytes on queue */
	unsigned short msg_qnum;	/* number of messages in queue */
	unsigned short msg_qbytes;	/* max number of bytes on queue */
	__kernel_ipc_pid_t msg_lspid;	/* pid of last msgsnd */
	__kernel_ipc_pid_t msg_lrpid;	/* last receive pid */
};
  • msg_perm:ipc_perm结构的一个实例,用于存放消息队列的许可权限信息,其中包括访问许可信息,以及队列创建者的有关信息

3.3 结构ipc_perm

struct ipc_perm
{
	__kernel_key_t	key;		//用于区分消息队列
	__kernel_uid_t	uid;		//消息队列用户的ID号
	__kernel_gid_t	gid;		//消息队列用户组的ID号
	__kernel_uid_t	cuid;		//消息队列创建者的ID号
	__kernel_gid_t	cgid;		//消息队列创建者的组ID号
	__kernel_mode_t	mode; 		//权限
	unsigned short	seq;		//序列号
};

 

3.4. 键值构建ftok()函数

ftok()函数将路径名和项目的标识符转变为一个系统V的IPC键值

key_t ftok(const char *pathname, int proj_id)

  • pathname:必须是已经存在的目录
  • proj_id:8位的值,通常用a,b等表示

 

3.5 获得消息msgget()函数

创建一个新的消息队列,或者访问一个现有的队列

#include <sys/types.h>

#include <sys/ipc.h>

#include <sys/msg.h>

int msgget(key_t kint msgget(key_t key, int msgflg)

  • key:ftok()函数生成的键值
  • msgflg:
    • IPC_CREAT:如果在内核中不存在该队列,则创建它。
    • IPC_EXCL:当与IPC_CREAT一期使用时,如果队列早已存在则msg将出错

 

3.6 发送消息msgsnd()函数

一但获得了队列标识符,用户就可以在该消息队列上执行相关操作了,msgsnd()函数向队列传递消息

#include <sys/types.h>

#include <sys/ipc.h>

#include <sys/msg.h>

int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);

  • msgid:队列标识符,调用msgget()获得的返回值
  • msgp:void类型的指针,指向一个消息缓冲区
  • msggz:包含消息的大小,以字节为单位,不包括消息类型的长度
  • msgflg:设置位为0表示忽略,也可以设置为IPC_NOWAIT

 

3.7 接受消息msgrcv()函数

当获得队列标识符后,就可以开始在该消息队列上执行消息队列的接受操作,msgrcv()函数用于接收队列标识符中的消息

#include <sys/types.h>

#include <sys/ipc.h>

#include <sys/msg.h>

ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);

  • msgid:在消息获取过程中所使用的队列,调用msgget的返回值
  • msgp:缓冲区变量的地址,存放获取的消息
  • msgsz:缓冲区结构的大小,不包括mtype成员的长度
  • msgtyp:指定要从队列中获取的消息类型

 

3.8 消息控制msgctl()函数

为了在一个消息队列上执行控制操作,可以使用msgctl()函数

#include <sys/types.h>

#include <sys/ipc.h>

#include <sys/msg.h>

int msgctl(int msqid, int cmd, struct msqid_ds *buf)

msgctl()向内核发送一个cmd命令,内核根据此来判断进行何种操作,buf为应用层和内核空间进行数据交换的指针

cmd值:

  • IPC_STAT:获取队列的msgid_ds结构,并把它存放在buf变量所指的地址中,通过这种方式,应用层可以获得当前消息队列的设置情况,例如是否有消息到来,消息队列的缓冲区设置等
  • IPC_SET:设置队列的msgid_ds结构的ipc_perm成员值,他是从buf中取得该值,通过IPC_SET命令,应用层可以设置消息队列的状态
  • IPC_RMID:内核删除队列,使用此命令后,内核会把此消息队列从系统中删除

 

4,消息队列的例子

本例在建立消息队列后,打印其属性,并在每次发送和接收后均查看其属性,最后对消息队列进行修改5

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <time.h>
#include <stdlib.h>
#include <string.h>

void msg_show_attr(int msg_id, struct msqid_ds msg_info)																	/*打印消息属性的函数*/
{
    int ret = -1;
    sleep(1);
    ret = msgctl(msg_id, IPC_STAT, &msg_info);		/*获取消息*/
    if( -1 == ret)
    {
        printf("获得消息信息失败\n");					/*获取消息失败,返回*/
        return ;
    }

    printf("\n");									/*以下打印消息的信息*/
    printf("现在队列中的字节数:%ld\n",msg_info.msg_cbytes);																	/*消息队列中的字节数*/
    printf("队列中消息数:%d\n",(int)msg_info.msg_qnum);	/*消息队列中的消息数*/
    printf("队列中最大字节数:%d\n",(int)msg_info.msg_qbytes);																	/*消息队列中的最大字节数*/
    printf("最后发送消息的进程pid:%d\n",msg_info.msg_lspid);																/*最后发送消息的进程*/
    printf("最后接收消息的进程pid:%d\n",msg_info.msg_lrpid);																/*最后接收消息的进程*/
    printf("最后发送消息的时间:%s",ctime(&(msg_info.msg_stime)));														/*最后发送消息的时间*/
    printf("最后接收消息的时间:%s",ctime(&(msg_info.msg_rtime)));														/*最后接收消息的时间*/
    printf("最后变化时间:%s",ctime(&(msg_info.msg_ctime)));																/*消息的最后变化时间*/
    printf("消息UID是:%d\n",msg_info.msg_perm.uid);	/*消息的UID*/
    printf("消息GID是:%d\n",msg_info.msg_perm.gid);	/*消息的GID*/
}

int main(void)
{
    int ret = -1;
    int msg_flags, msg_id;
    key_t key;
    struct msgmbuf{             //消息的缓冲区结构
        int mtype;
        char mtext[10];
    };
    struct msqid_ds msg_info;
    struct msgmbuf msg_mbuf;
    memset(msg_mbuf.mtext, 0, 10);
    int msg_sflags, msg_rflags;
    char *msgpath = "/ipc/msg/";    //消息key产生所用的路径
    key = ftok(msgpath, 'b');
    if (key != -1)
    {
        printf("成功建立KEY\n");
    }
    else
    {
        printf("建立KEY失败/n");
    }

    msg_flags = IPC_CREAT|IPC_EXCL;     //消息的类型
    msg_id = msgget(key, msg_flags|0x0666);    //建立消息
    if (-1 == msg_id)
    {
        printf("建立消息失败\n");
        return 0;
    }
    msg_show_attr(msg_id,msg_info);     //显示消息属性

    msg_sflags = IPC_NOWAIT;
    msg_mbuf.mtype = 10;
    memcpy(msg_mbuf.mtext, "123456", sizeof("123456"));
    ret = msgsnd(msg_id, &msg_mbuf, sizeof("123456"), msg_sflags);
    if (-1 == ret)
    {
        printf("发送消息失败\n");
    }
    msg_show_attr(msg_id, msg_info);    //显示消息属性

    msg_rflags = IPC_NOWAIT|MSG_NOERROR;
    ret = msgrcv(msg_id, &msg_mbuf, 10, 10, msg_rflags); // 接收消息
    if (-1 == ret)
    {
        printf("接收消息失败\n");
    }
    else
    {
        printf("接收消息成功,长度%d\n", ret);
    }
    msg_show_attr(msg_id, msg_info);    //显示消息属性

    msg_info.msg_perm.uid = 8;
    msg_info.msg_perm.gid = 8;
    msg_info.msg_qbytes = 12345;
    ret = msgctl(msg_id, IPC_SET, &msg_info); //设置消息属性
    if (-1 == ret)
    {
        printf("设置消息属性失败\n");
        return 0;
    }
    msg_show_attr(msg_id, msg_info);

    ret = msgctl(msg_id, IPC_RMID, NULL);
    if (-1 == ret)
    {
        printf("删除消息失败\n");
        return 0;
    }

    return 0;
}

接收消息失败

 

5,信号量

        信号量是一种计数器,用来控制对多个进程共享的资源所进行的访问。它们常常被用做一个锁机制,在某个进程正在对特定资源进行操作时,信号量可以防止另一个进程去访问它。生产者和消费者的模型是信号量的典型使用

 

5.1 信号量数据结构

union semun{ 
    int 			val; 		//信号量初始值 
    struct semid_ds 	*buf;  		//semid_ds结构指针
    unsigned short int 	*array;   	//数组类型
    struct seminfo 	*__buf;      	//信号量内部结构
}; 

 

5.2 新建信号量函数semget()

semget()函数用于创建一个新的信号量集合,或者访问现有的集合

#include <sys/types.h>

#include <sys/ipc.h>

#include <sys/sem.h>

int semget(key_t key, int nsems, int semflg);

  • key:ftok生成的键值
  • nsems:指定在新的集合中应该创建的信号量的数目
  • semflg:打开信号量的方式
    • IPC_CREAT:如果在内核中不存在这样的信号量集合,则创建它。
    • IPC_EXCL:当与IPC_CREAT一期使用时,如果信号量集合早已存在则操作将出错

 

5.3 信号量操作函数semop()

信号量的P,V操作是通过向已经建立好的信号量,发送命令来完成的。向信号量发送命令的函数是semop()。

#include <sys/types.h>

#include <sys/ipc.h>

#include <sys/sem.h>

int semop(int semid, struct sembuf *sops, unsigned nsops);

  • semid:semget的返回值
  • sops:指向将要在信号量集合上执行操作的一个数组
  • nsops:该数组中操作的个数

 

struct sembuf
{
  	unsigned short sem_num;		//信号量的编号
  	short sem_op;			//信号量的操作
  	short sem_flg;			//信号量的操作标志
};

struct sembuf sem = {0, +1, NOWAIT}; //表示对信号量0,进行加1的操作
struct sembuf sem = {0, -1, NOWAIT}; //表示对信号量0,进行减1的操作

 

5.4 控制信号量参数semctl()函数

#include <sys/types.h>

#include <sys/ipc.h>

#include <sys/sem.h>

int semctl(int semid, int semnum, int cmd, …);

  • semid:调用semget()的返回值
  • semnum:将要执行操作的信号量的编号
  • cmd:将要在集合上执行的操作

cmd取值:

  • IPC_STAT:获取某个集合的semid_ds结构,并把它存储在semun联合体的buf参数所指定的地址中
  • IPC_SET:设置某个集合的semid_ds结构的ipc_perm成员的值。该命令所取的值是从semun联合体的buf参数中取到的
  • IPC_RMID:从内核删除该集合
  • GETALL:用于获取集合中所有信号量的值。整数值存放在无符号短整数的一个数组中,该数组由联合体的array成员指定
  • GETNCNT:返回当前正在等待资源的进程的数目
  • GETPID:返回最后一次执行semop调用的进程的PID
  • GETVAL:返回集合中某个信号量的值
  • GETZCNT:返回正在等待资源利用率达到百分之百的进程的数目
  • SETALL:把集合中所有信号量的值,设置为联合体的array成员所包含的对应值
  • SETVAL:把集合中单个信号量的值设置为联合体的val成员的值。

 

5.5 信号量操作的例子

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/sem.h>
#include <sys/ipc.h>

typedef int sem_t;
union semun{
    int 			val; 		//信号量初始值
    struct semid_ds 	*buf;  		//semid_ds结构指针
    unsigned short int 	*array;   	//数组类型
    struct seminfo 	*__buf;      	//信号量内部结构
};

sem_t CreateSem(key_t key, int value)       //创建信号量
{
    union semun sem;
    sem_t semid;
    sem.val = value;

    printf("%d\n", key);
    semid = semget(key, 0, IPC_CREAT|0666);
    if (-1 == semid)
    {
        printf("create semphore error\n");
        return -1;
    }

    semctl(semid, 0, SETVAL, sem);
    
    return semid;
}

int Sem_P(sem_t semid)          //增加信号量
{
    struct sembuf sops = {0, +1, IPC_NOWAIT};

    return (semop(semid, &sops, 1));
}

int Sem_V(sem_t semid)          //减小信号量
{
    struct sembuf sops = {0, -1, IPC_NOWAIT};

    return (semop(semid, &sops, 1));
}

void SetvalueSem(sem_t semid, int value)        //设置信号量值
{
    union semun sem;
    sem.val = value;

    semctl(semid, 0, SETVAL, sem);
}

int GetvalueSem(sem_t semid)                    //获取信号量值
{
    union semun sem;
    return semctl(semid, 0, GETVAL, sem);
}

void DestorySem(sem_t semid)                    //摧毁信号量
{
    union semun sem;
    sem.val = 0;

    semctl(semid, 0, IPC_RMID, sem);
}

int main(void)
{
    key_t key;
    sem_t semid;

    char i;
    int value = 0;

    key = ftok("/ipc/sem", 'a');
    semid = CreateSem(key, 100);

    for(i=0; i<3; i++)
    {
        Sem_P(semid);
        Sem_V(semid);
    }

    value = GetvalueSem(semid);
    printf("信号量的值为%d\n", value);

    DestorySem(semid);

    return 0;
}


信号量创建失败

 

6,共享内存

       共享内存是在多个进程之间共享内存区域的一种进程间的通信方式,它是在多个进程之间对内存段进行映射的方式实现内存共享的,是IPC最快捷的方式。

6.1 创建共享内存函数shmget()

shmget()用于创建一个新的共享的内存段,或者访问一个现有的共享内存段。

#include <sys/ipc.h>

#include <sys/shm.h>

int shmget(key_t key, size_t size, int shmflg);

  • key :ftok()生成的键值
  • shmflg
    • IPC_CREAT:如果在内核中不存在该内存段,则创建它。
    • IPC_EXCL:当与IPC_CREAT一期使用时,如果该内存段早已存在则操作将出错

 

6.2 获得共享内存地址函数shmat()

#include <sys/ipc.h>

#include <sys/shm.h>

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

int shmdt(const void *shmaddr);

如果shmaddr参数值等于0,则内核将试着查找一个未映射的区域,用户可以指定一个地址,但通常该地址只用于访问所拥有的硬件,或者解决与其他应用程序的冲突。

SHM_RND标志可以与标志参数进行OR操作,结果在置为标志参数,这样可以让传送的地址页对齐

 

6.3 删除共享内存函数shmdt()

#include <sys/ipc.h>

#include <sys/shm.h>

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

int shmdt(const void *shmaddr);

当某进程不在需要一个共享内存段时,它必须调用这个函数来断开与该内存段的连接。在成功完成了断开连接操作以后,相关的shmid_ds结构的shm_nattch成员的值将减去1.如果这个值减到0,则内核将真正删除该内存段。

 

6.4 共享内存控制函数shmctl()

#include <sys/ipc.h>

#include <sys/shm.h>

int shmctl(int shmid, int cmd, struct shmid_ds *buf);

向共享内存的句柄发送命令来完成某种功能

命令值:

  • IPC_SET:获取内存段的shmid_ds结构,并把它存储在buf参数所指定的地址中
  • IPC_RMID:标记某内存段,已备删除。

 

7,信号

       信号机制时UNIX系统中最为古老的进程之间的通信机制。它用于在一个或多个进程之间传递异步信号。信号可以由各种异步事件产生,例如键盘中断等。shell也可以使用信号将作业控制命令传递给它的子进程。

        kill -l 列出所有的信号

 

7.1 信号截取函数signal()

signal()函数用于截取系统的信号,对此信号挂接用户自己的处理函数。

#include <signal.h>

typedef void (*sighandler_t)(int);

sighandler_t signal(int signum, sighandler_t handler);

signal()函数的原型说明此函数要求两个参数,返回一个函数指针,而该指针所指向的函数无返回值。第一个参数signo是一个整型数,第二个参数是函数指针,他所指向的函数需要一个整形参数,无返回值

 

7.2 向进程发送信号函数kill()和raise()

在挂接信号处理函数后,可以等待系统信号的到来。同时,用户可以自己构建信号发送到目标进程中。

#include <sys/type.h>

#include <signal.h>

int kill(pid_t pid, int sig);

int raise(int sig);

kill()函数向进程号为pid的进程发送信号,信号值为sig。当pid为0时,向当前系统的所有进程发送信号sig,即群发的意思。raise()函数在当前进程中自举一个信号sig,即向当前进程发送信号

 

四,Linux下的线程

1,多线程编程

1.1 多线程编程优点

  1. 系统资源消耗低
  2. 速度快
  3. 线程间的数据共享比进程间容易的多

 

1.2 编写Linux下的线程需要包含头文件pthread.h,在生成可执行文件的时候需要链接库libpthread.a或者libpthread.so

 

2,Linux下线程创建函数pthread_create()

#include <pthread.h>

int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);

  • thread:用于表示一个线程
  • attr:设置线程的属性,设置为空时采用默认属性
  • start_routine:当线程的资源分配成功后,线程中所运行的单元
  • arg:线程函数运行时传入的参数
  • 返回值:线程创建成功返回0,若不为0,则说明创建线程失败,常见的错误返回码为EAGAIN和EINVAL。EAGAIN表示系统中的线程数量达到了上限,EINVAL表示线程的属性非法。

 

3,线程的结束函数pthread_join()和pthread_exit()

3.1 函数pthread_join()用来等待一个线程运行结束。这个函数是阻塞函数,一直到被等待的线程结束为止,函数才返回并且收回被等待的资源

#include <pthread.h>

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

  • thread:线程的标识符,pthread_create()创建成功的返回值
  • retval:线程返回值,存储被等待线程的返回值

 

3.2 线程通过pthread_exit()结束,将结果传出

#include <pthread.h>

void pthread_exit(void *retval);

retval:函数的返回值,可以被pthread_join捕获。

 

4,线程的属性

       在用pthread_create()函数创建线程时,使用了默认参数,即将该函数的第二个参数设置为NULL。通常来说,建立一个线程的时候,使用默认属性就够了,但是很多shihou时候需要调整线程的属性,特别是线程的优先级。

4.1 线程的属性结构

 typedef struct{
 	int 			detachstate;     	//线程的分离状态
 	int                     schedpolicy;   		//线程调度优先级                       
 	struct sched_param      schedparam;   		//线程的调度参数                       
 	int                     inheritsched;    	//线程的继承性                      
  	int                     scope;          	//线程的作用域                      
    size_t                  guardsize; 		//线程栈末尾的警戒缓冲区大小                     
    int                     stackaddr_set;          //运行栈             
    void *                  stackaddr;      	//线程栈的位置                       
    size_t                  stacksize;       	//线程栈的大小                
}pthread_attr_t;

线程的属性不能直接设置,需使用相关函数进行操作。线程属性的初始化函数为pthread_attr_init(),这个函数必须在pthread_create()之前调用。

属性对象主要包括线程的摘取状态,调度优先级,运行栈地址,运行栈大小,优先级

4.2 线程的优先级

线程的优先级由两个函数控制

  1. pthread_attr_getschedparam() :获得线程的优先级设置

int pthread_attr_getschedparam(const pthread_attr_t *attr, struct sched_param *param);

  1. pthread_attr_setschedparam() :设置线程的优先级

int pthread_attr_setschedparam(pthread_attr_t *attr, const struct sched_param *param);

线程的优先级存放在结构sched_param中。其操作方式是先将优先级取出来,然后对需要设置的参数修改后再写回去,这是对复杂结构进行设置的通用办法,防止因为设置不当造成不可预料的麻烦

4.3 线程的绑定状态

#include <pthread.h>

int pthread_attr_setscope(pthread_attr_t *attr, int scope);

int pthread_attr_getscope(const pthread_attr_t *attr, int *scope);

  • attr:指向属性结构指针
  • scope:绑定类型,有两个取值:PTHREAD_SCOPE_SYSTEM(绑定的)和PTHREAD_SCOPE_PROCESS(非绑定的)

 

4.4 线程的分离状态

线程的分离状态有分离线程和非分离线程,分离线程不用其他线程等待,当前线程运行结束后线程就结束了,并且马上释放资源。

#include <pthread.h>

int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);

int pthread_attr_getdetachstate(const pthread_attr_t *attr, int *detachstate);

detachstate可以为分离线程或者非分离线程,PTHREAD_CREATE_DETACHED用于设置分离线程,PTHREAD_CREATE_JOINABLE用于设置非分离线程

 

5,线程间的互斥

互斥是用来保护一段临界区的,它可以保证某时间段内只有一个线程在执行一段代码或者访问某个资源

5.1 线程互斥的函数介绍

互斥的初始化函数:pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t * mutexattr)

互斥的锁定函数: pthread_mutex_lock(pthread_mutex_t *mutex)

互斥的预锁定函数:pthread_mutex_trylock(pthread_mutex_t *mutex)

互斥的解锁函数: pthread_mutex_unlock(pthread_mutex_t *mutex)

互斥的销毁函数: pthread_mutex_destroy(pthread_mutex_t *mutex)

函数pthread_mutex_init(),初始化一个mutex变量,函数pthread_mutex_lock()函数声明开始用互斥锁上锁,此后的代码直至调用pthread_mutex_unlock()函数为止,均不能执行被保护区域的代码。互斥锁使用完毕后要调用pthread_mutex_destory释放资源。

5.2 线程互斥函数的例子

该例子使用线程互斥的方法构建生产者和消费者,代码中建立了两个线程,函数producter_f()用于生产,函数consumer_f()用于消费。

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <sched.h>

int buffer_has_item = 0;        //缓冲区计数值
pthread_mutex_t mutex;
int running = 1;

void *producter_f(void *arg)
{
    while(running)
    {
        pthread_mutex_lock(&mutex);
        buffer_has_item++;
        printf("生产,总数量:%d\n",buffer_has_item);
        pthread_mutex_unlock(&mutex);
    }
}

void *consumer_f(void *arg)
{
    while(running)
    {
        pthread_mutex_lock(&mutex);
        buffer_has_item--;
        printf("生产,总数量:%d\n",buffer_has_item);
        pthread_mutex_unlock(&mutex);
    }
}

int main()
{
    pthread_t consumer_t;       //消费者线程参数
    pthread_t producter_t;      //生产者线程参数

    pthread_mutex_init(&mutex, NULL);

    pthread_create(&producter_t, NULL, (void *)producter_f, NULL);  //建立生产者线程
    pthread_create(&consumer_t, NULL, (void *)consumer_f, NULL);    //建立消费者线程

    usleep(1);
    running = 0;
    pthread_join(consumer_t, NULL);
    pthread_join(producter_t, NULL);
    pthread_mutex_destroy(&mutex);

    return 0;
}

6,线程中使用信号量

信号量实际上是一个非负的整数计数器,用来实现对公共资源的控制。在公共资源增加的时候,信号量的值增加;公共资源消耗的时候,信号量的值减少;只有当信号量的值大于0的时候,才能访问信号量所代表的公共资源。

link -pthread

6.1 线程信号量初始化函数sem_init()

#include <semaphore.h>

int sem_init(sem_t *sem, int pshared, unsigned int value);

  • sem:指向信号量结构的一个指针,当信号量初始化成功的时候,可以使用这个指针进行信号量的增加减少操作;
  • pshared:表示信号量的共享类型,不为0时这个信号量可以在进程间共享,否则这个信号量只能在当前进程的多个线程间共享
  • value:设置信号量初始化时候信号量的值

 

6.2 线程信号量增加函数sem_post()

该函数的作用是增加信号量的值,每次增加的值为1.当有线程等待这个信号量的时候,等待的线程将返回。

#include <semaphore.h>

int sem_post(sem_t *sem);

 

6.3 线程信号量等待函数sem_wait()

该函数的作用是减少信号量的值,如果信号量的值为0,则线程会一直阻塞到信号量的值大于0为止。每次使信号量的值减少1,当信号量的值为0时不在减少。

#include <semaphore.h>

int sem_wait(sem_t *sem);

int sem_trywait(sem_t *sem);

int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);

 

6.4 线程信号量摧毁函数sem_destory()

该函数用来释放信号量sem

#include <semaphore.h>

int sem_destroy(sem_t *sem);

 

6.4 线程信号量的例子

一个线程增加信号量模仿生产者,一个线程减小信号量模仿消费者

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <semaphore.h>

sem_t sem;
int running = 1;

void *producter_f(void *arg)
{
    int semval = 0;
    while(running)
    {
        usleep(1);
        sem_post(&sem);
        sem_getvalue(&sem, &semval);
        printf("生产,总数量:%d\n",semval);
    }
}

void *consumer_f(void *arg)
{
    int semval = 0;
    while(running)
    {
        usleep(1);
        sem_wait(&sem);
        sem_getvalue(&sem, &semval);
        printf("消费,总数量:%d\n",semval);
    }
}

int main()
{
    pthread_t consumer_t;       //消费者线程参数
    pthread_t producter_t;      //生产者线程参数

    sem_init(&sem, 0, 16);

    pthread_create(&producter_t, NULL, (void *)producter_f, NULL);  //建立生产者线程
    pthread_create(&consumer_t, NULL, (void *)consumer_f, NULL);    //建立消费者线程

    sleep(1);
    running = 0;
    pthread_join(consumer_t, NULL);
    pthread_join(producter_t, NULL);
    sem_destroy(&sem);

    return 0;
}
 

数值以交叉方式进行,有的时候产生多个在消耗多个。


版权声明:本文为qq_43657810原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。