操作系统学习笔记(二)

发布时间:2024年01月02日

1.进程切换为什么比线程更消耗资源?

? 进程切换时需要刷新TLB并获取新的地址空间,然后切换硬件上下文和内核栈;线程切换时只需要切换硬件上下文和内核栈。

? 解析:

? 进程是程序的动态表现。 一个程序进行起来后,会使用很多资源,比如使用寄存器,内存,文件等。每当切换进程时,必须要考虑保存当前进程的状态。状态包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开的文件描述符的集合,这个状态叫做上下文(Context)。可见,想要切换进程,保存的状态还不少。不仅如此,由于虚拟内存机制,进程切换时需要刷新TLB并获取新的地址空间。

? 线程存在于进程中,一个进程可以有一个或多个线程。线程是运行在进程上下文中的逻辑流,这个线程可以独立完成一项任务。同样线程有自己的上下文,包括唯一的整数线程ID, 栈、栈指针、程序计数器、通用目的寄存器和条件码。可以理解为线程上下文是进程上下文的子集。

? 由于保存线程的上下文明显比进程的上下文小,因此系统切换线程时,必然开销更小。

2. 介绍一下进程之间的通信。

? 为了提高计算机系统的效率.增强计算机系统内各种硬件的并行操作能力.操作系统要求程序结构必须适应并发处理的需要.为此引入了进程的概念。而进程并行时,需要考虑进程间的通信,进程间通信主要有以下几种方式:匿名管道、命名管道、信号、消息队列、共享内存、信号量、Socket。

  1. ? 匿名管道:管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。
#include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <string.h> #include <sys/wait.h>  int pipe_default[2];  int main() {   pid_t pid;   char buffer[32];    memset(buffer, 0, 32);   if(pipe(pipe_default) < 0)  {     printf("Failed to create pipe!\n");     return 0;  }    if(0 == (pid = fork()))  {     close(pipe_default[1]); //关闭写端     sleep(2);     if(read(pipe_default[0], buffer, 32) > 0)     {       printf("[Client] Receive data from server: %s \n", buffer);     }     close(pipe_default[0]);    }   else  {     close(pipe_default[0]);  //关闭读端     char msg[32]="== hello world ==";     if(-1 != write(pipe_default[1], msg, strlen(msg)))     {       printf("[Server] Send data to client: %s \n",msg);     }     close(pipe_default[1]);     waitpid(pid, NULL, 0);  }   return 1; }
  1. ? 有名管道

    ? 匿名管道,由于没有名字,只能用于亲缘关系的进程间通信。为了克服这个缺点,提出了有名管道(FIFO)。

    ? 有名管道不同于匿名管道之处在于它提供了一个路径名与之关联,以有名管道的文件形式存在于文件系统中,这样,即使与有名管道的创建进程不存在亲缘关系的进程,只要可以访问该路径,就能够彼此通过有名管道相互通信,因此,通过有名管道不相关的进程也能交换数据。值的注意的是,有名管道严格遵循先进先出(first in first out) ,对匿名管道及有名管道的读总是从开始处返回数据,对它们的写则把数据添加到末尾。它们不支持诸如lseek()等文件定位操作。有名管道的名字存在于文件系统中,内容存放在内存中。

  2. ? 信号

  • ? 信号是Linux系统中用于进程间互相通信或者操作的一种机制,信号可以在任何时候发给某一进程,而无需知道该进程的状态。
  • ? 如果该进程当前并未处于执行状态,则该信号就有内核保存起来,知道该进程回复执行并传递给它为止。
  • ? 如果一个信号被进程设置为阻塞,则该信号的传递被延迟,直到其阻塞被取消是才被传递给进程。

? 以下列出几个常用的信号:

信号描述
SIGHUP当用户退出终端时,由该终端开启的所有进程都退接收到这个信号,默认动作为终止进程。
SIGINT程序终止(interrupt)信号, 在用户键入INTR字符(通常是Ctrl+C)时发出,用于通知前台进程组终止进程。
SIGQUIT和SIGINT类似, 但由QUIT字符(通常是Ctrl+)来控制. 进程在因收到SIGQUIT退出时会产生core文件, 在这个意义上类似于一个程序错误信号。
SIGKILL用来立即结束程序的运行. 本信号不能被阻塞、处理和忽略
SIGTERM程序结束(terminate)信号, 与SIGKILL不同的是该信号可以被阻塞和处理。通常用来要求程序自己正常退出。
SIGSTOP停止(stopped)进程的执行. 注意它和terminate以及interrupt的区别:该进程还未结束, 只是暂停执行. 本信号不能被阻塞, 处理或忽略.

? 代码示例:

? 下面的代码收到程序退出信号后会执行用户定义的信号处理函数来替代系统默认的处理程序。

#include<stdlib.h> #include<stdio.h> #include<signal.h> #include<sys/types.h> #include<unistd.h>  void sig_handle(int sig) {     printf("received signal: %d, quit.\n", sig);     exit(0); }  int main () {     signal(SIGINT, sig_handle);     signal(SIGKILL, sig_handle);     signal(SIGSEGV, sig_handle);     signal(SIGTERM, sig_handle);       int i = 0;      while (1) {          printf("%d\n", ++i);          sleep(2);      }       printf("main quit.");       return 0; }

? 运行结果:

1 2 received signal: 15, quit.
  1. ? 消息队列
  • ? 消息队列是存放在内核中的消息链表,每个消息队列由消息队列标识符表示。
  • ? 与管道(无名管道:只存在于内存中的文件;命名管道:存在于实际的磁盘介质或者文件系统)不同的是消息队列存放在内核中,只有在内核重启(即,操作系统重启)或者显示地删除一个消息队列时,该消息队列才会被真正的删除。
  • ? 另外与管道不同的是,消息队列在某个进程往一个队列写入消息之前,并不需要另外某个进程在该队列上等待消息的到达

? 消息队列特点总结:

? (1)消息队列是消息的链表,具有特定的格式,存放在内存中并由消息队列标识符标识.

? (2)消息队列允许一个或多个进程向它写入与读取消息.

? (3)管道和消息队列的通信数据都是先进先出的原则。

? (4)消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取.比FIFO更有优势。

? (5)消息队列克服了信号承载信息量少,管道只能承载无格式字 节流以及缓冲区大小受限等缺。

? (6)目前主要有两种类型的消息队列:POSIX消息队列以及System V消息队列,系统V消息队列目前被大量使用。系统V消息队列是随内核持续的,只有在内核重起或者人工删除时,该消息队列才会被删除。

  1. ? 共享内存

    ? 进程间本身的内存是相互隔离的,而共享内存机制相当于给两个进程开辟了一块二者均可访问的内存空间,这时,两个进程便可以共享一些数据了。但是,多进程同时占用资源会带来一些意料之外的情况,这时,我们往往会采用上述的信号量来控制多个进程对共享内存空间的访问。

    #include <iostream> #include <stdlib.h> #include <string.h> #include <sys/shm.h> #include <sys/ipc.h> #include <unistd.h>  using namespace std; int main() {   char *shmaddr;   char *shmaddread;   char str[]="Hello, I am a processing. \n";   int shmid;    key_t key = ftok(".",1);   pid_t pid1 = fork();   if(pid1 == -1){      cout << "Fork error. " << endl;      exit(1);  }   else if(pid1 == 0){      //子进程      shmid = shmget(key,1024,IPC_CREAT | 0600);      shmaddr = (char*)shmat(shmid, NULL, 0);                 strcpy(shmaddr, str);      cout << "[Writer] write: " << shmaddr << endl;      shmdt(shmaddr);  }   else  {      //父进程      pid_t pid2 = fork();      if(pid2 == -1){        cout << "Fork error. " << endl;        exit(1);     }     else if(pid2 == 0){        //子进程        sleep(2);        shmid = shmget(key,1024,IPC_CREAT | 0600);        shmaddread = (char*)shmat(shmid, NULL, 0);                cout << "[Reader] read: " << shmaddread << endl;        shmdt(shmaddread);     }  }   sleep(3);   return 0; }
    
  2. ? 信号量

    ? 信号量主要用来解决进程和线程间并发执行时的同步问题,进程同步是并发进程为了完成共同任务采用某个条件来协调他们的活动,这是进程之间发生的一种直接制约关系。

    ? 对信号量的操作分为P操作和V操作,P操作是将信号量的值减一,V操作是将信号量的值加一。当信号量的值小于等于0之后,再进行P操作时,当前进程或线程会被阻塞,直到另一个进程或线程执行了V操作将信号量的值增加到大于0之时。锁也是用的这种原理实现的。

    ? 信号量我们需要定义信号量的数量,设定初始值,以及决定何时进行PV操作。

     #include <unistd.h>    #include <sys/types.h>    #include <sys/stat.h>    #include <fcntl.h>    #include <stdlib.h>    #include <stdio.h>    #include <string.h>    #include <sys/sem.h>  #define KEY (key_t)15030110070  #define N 20   static void p(int semid ,int semNum);    static void v(int semid ,int semNum);   union semun {        int val;        struct semid_ds *buf;        ushort *array;    };     int main(int argc ,char* argv[])  {    int i;    int semid;     semid = semget(KEY,3,IPC_CREAT|0660);      union semun arg[3];      arg[0].val = 1;                     //mutex  [0]  对缓冲区进行操作的互斥信号量    arg[1].val = N;               //empty  [1]  缓冲区空位个数n    arg[2].val = 0;                     //full [2]  产品个数       for(i=0;i<3;i++)            semctl(semid,i,SETVAL,arg[i]);       pid_t p1,p2;    if((p1=fork()) == 0)    {      //子进程1,消费者      while(1)      {        printf("消费者 1 等待中...\n");        sleep(2);        int product = rand() % 2 + 1;        for(int i = 0; i < product; i++)        {          p(semid ,2);    //消费          p(semid ,0);    //加锁          printf(" [消费者 1] 消费产品 1. 剩余:  %d\n", semctl(semid, 2, GETVAL, NULL));          v(semid ,0);    //开锁          v(semid ,1);    //释放空位        }        sleep(2);      }      }    else    {      if((p2=fork()) == 0)      {        //子进程2,消费者        while(1)        {          printf("消费者 2 等待中...\n");          sleep(2);          int product = rand() % 2 + 1;          for(int i = 0; i < product; i++)          {            p(semid ,2);    //消费            p(semid ,0);    //加锁            printf(" [消费者 2] 消费产品 1. 剩余:  %d\n", semctl(semid, 2, GETVAL, NULL));            v(semid ,0);    //开锁            v(semid ,1);    //释放空位          }          sleep(2);        }      }      else      {        //父进程,生产者        while(1)        {          printf("生产者开始生产...\n");          int product = rand() % 5 + 1;           for(int i = 0; i < product; i++)          {            p(semid ,1);    //占用空位            p(semid ,0);    //加锁            printf(" [生产者] 生产产品 1. 剩余:  %d\n", semctl(semid, 2, GETVAL, NULL) + 1);              v(semid ,0);    //开锁            v(semid, 2);    //生产          }          sleep(2);        }      }    }    return 0;  }   /* p操作 */    void p(int semid ,int semNum)  {        struct sembuf sb;        sb.sem_num = semNum;        sb.sem_op = -1;        sb.sem_flg = SEM_UNDO;        semop(semid, &sb, 1);    }     /* v操作 */    void v(int semid ,int semNum)  {        struct sembuf sb;        sb.sem_num = semNum;        sb.sem_op = 1;        sb.sem_flg = SEM_UNDO;        semop(semid, &sb, 1);    }  
    
  3. ? socket

    ? img

    ? 套接字可以看做是:不同主机之间的进程进行双向通信的端点。(套接字 = IP地址 + 端口号)

3.介绍一下信号量。

  1. ? 在多进程环境下,为了防止多个进程同时访问一个公共资源而出现问题,需要一种方法来协调各个进程,保证它们能够合理地使用公共资源。信号量就是这样一种机制。

    ? 信号量的数据类型为结构sem_t,它本质上是一个长整型的数。函数sem_init()用来初始化一个信号量。它的原型为:

    ? extern int sem_init *P ((sem_t **sem, int _*pshared, unsigned int _*value));

    ? sem为指向信号量结构的一个指针;pshared不为0时此信号量在进程间共享,否则只能为当前进程的所有线程共享;value给出了信号量的初始值。

    ? (1)函数sem_post( sem_t *sem )用来增加信号量的值。当有线程阻塞在这个信号量上时,调用这个函数会使其中的一个线程不在阻塞,选择机制同样是由线程的调度策略决定的。

    ? (2)函数sem_wait( sem_t *sem )被用来阻塞当前线程直到信号量sem的值大于0,解除阻塞后将sem的值减一,表明公共资源经使用后减少。函数sem_trywait ( sem_t *sem )是函数sem_wait()的非阻塞版本,它直接将信号量sem的值减一。

    ? (3)函数sem_timedwait(sem_t *sem, const struct timespec *abs_timeout) 与 sem_wait() 类似,只不过 abs_timeout 指定一个阻塞的时间上限,如果调用因不能立即执行递减而要阻塞。

    ? (4)函数sem_destroy(sem_t *sem)用来释放信号量sem。

  2. ? 使用示例代码如下

//g++ semtest.cpp -o test -lpthread #include <stdio.h> #include <semaphore.h> #include <pthread.h> #include <unistd.h> #include <sys/time.h> sem_t sem;  /*function:获取当前时间,精确到毫秒? * */ int64_t getTimeMsec() {     struct  timeval    tv;     gettimeofday(&tv, NULL);     return tv.tv_sec * 1000 + tv.tv_usec / 1000; }  void* func_sem_wait(void* arg) {     printf("set wait\n");     sem_wait(&sem);     printf("sem wait success\n");     int *running = (int*)arg;     printf("func_sem_wait running\n");     printf("%d\n", *running); }  void* func_sem_timedwait(void* arg) {     timespec timewait;     timewait.tv_sec = getTimeMsec() / 1000 + 2;     timewait.tv_nsec = 0;     printf("sem_timedwait\n");     int ret = sem_timedwait(&sem, &timewait);     printf("sem_timedwait,ret=%d\n", ret);     printf("func_sem_timedwait running\n"); }  void* func_sem_post(void* arg) {     printf("func_sem_post running\n");     printf("sem post\n");     int *a = (int*)arg;     *a = 6;     sem_post(&sem);     sem_post(&sem); }  int main() {     sem_init(&sem, 0, 0);     pthread_t thread[3];     int a = 5;      pthread_create(&(thread[0]), NULL, func_sem_wait, &a);     printf("thread func_sem_wait\n");      pthread_create(&(thread[2]), NULL, func_sem_timedwait, &a);     printf("thread func_sem_timedwait\n");      sleep(4);      pthread_create(&(thread[1]), NULL, func_sem_post, &a);     printf("thread func_sem_post\n");      pthread_join(thread[0], NULL);     pthread_join(thread[1], NULL);     pthread_join(thread[2], NULL);     sem_destroy(&sem); }

4.说说僵尸进程和孤儿进程。

  1. ? 我们知道在unix/linux中,正常情况下,子进程是通过父进程创建的,子进程在创建新的进程。子进程的结束和父进程的运行是一个异步过程,即父进程永远无法预测子进程 到底什么时候结束。 当一个 进程完成它的工作终止之后,它的父进程需要调用wait()或者waitpid()系统调用取得子进程的终止状态。
  2. ? 孤儿进程:一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。
  3. ? 僵尸进程:一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵尸进程。

5.请介绍进程之间的通信方式。

? 进程间通信主要有以下几种方式

  1. ? 管道pipe:管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。

    #include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <string.h> #include <sys/wait.h>  int pipe_default[2];  int main() { pid_t pid; char buffer[32];  memset(buffer, 0, 32); if(pipe(pipe_default) < 0) {   printf("Failed to create pipe!\n");   return 0; }  if(0 == (pid = fork())) {   close(pipe_default[1]); //关闭写端   sleep(2);   if(read(pipe_default[0], buffer, 32) > 0)  {     printf("[Client] Receive data from server: %s \n", buffer);  }   close(pipe_default[0]);  } else {   close(pipe_default[0]);  //关闭读端   char msg[32]="== hello world ==";   if(-1 != write(pipe_default[1], msg, strlen(msg)))  {     printf("[Server] Send data to client: %s \n",msg);  }   close(pipe_default[1]);   waitpid(pid, NULL, 0); } return 1; }
    
  2. ? 命名管道FIFO:有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。

    ? 写管道:

    #include <stdio.h> #include <errno.h> #include <unistd.h> #include <sys/types.h> #include <string.h> #include <stdlib.h> #include <fcntl.h> #include <sys/stat.h>  int main() {  int nFd = 0;  int nWrLen = 0, nReadLen = 0;;  char szBuff[BUFSIZ] = {0};   /* 打开当前目录下的管道文件 */  nFd = open("pipe", O_RDWR);  if (-1 == nFd)  {   perror("Open fifo failed\n");   return 1;  }   while (1)  {   /* 从终端读取数据 */   memset(szBuff,0,BUFSIZ);   nReadLen = read(STDIN_FILENO,szBuff,BUFSIZ);    if(nReadLen > 0)   {    /* 往管道写入数据 */    nWrLen = write(nFd, szBuff, strlen(szBuff)+1);    if (nWrLen > 0)    {     printf("write data successful: %s \n", szBuff);    }    else    {     perror("write failed:");    }   }  } }
    

? 读管道:

#include <stdio.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <string.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/stat.h>

int main()
{
    int nFd = 0;
    int  nReadLen = 0;;
    char szBuff[BUFSIZ] = {0};

    /* 打开当前目录下的管道文件 */
    nFd = open("pipe", O_RDWR);
    if (-1 == nFd)
    {
        perror("Open fifo failed\n");
        return 1;
    }

    while (1)
    {
        /* 从管道读取数据 */
        memset(szBuff,0,BUFSIZ);
        nReadLen = read(nFd,szBuff,BUFSIZ);
        if(nReadLen > 0)
        {
            printf("read pipe data: %s\n", szBuff);
        }   

    }
}
  1. ? 消息队列MessageQueue:消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。

    ? 示例:使用消息队列进行进程间通信

    ? 接收信息的程序源文件为msgreceive.c的源代码为:

    #include <unistd.h>
    #include <stdlib.h>
    #include <stdio.h>
    #include <string.h>
    #include <errno.h>
    #include <sys/msg.h>
    
    struct msg_st
    {
        long int msg_type;
        char text[BUFSIZ];
    };
    
    int main()
    {
        int running = 1;
        int msgid = -1;
        struct msg_st data;
        long int msgtype = 0; //注意1
    
        //建立消息队列
        msgid = msgget((key_t)1234, 0666 | IPC_CREAT);
        if(msgid == -1)
        {
            fprintf(stderr, "msgget failed with error: %d\n", errno);
            exit(EXIT_FAILURE);
        }
        //从队列中获取消息,直到遇到end消息为止
        while(running)
        {
            if(msgrcv(msgid, (void*)&data, BUFSIZ, msgtype, 0) == -1)
            {
                fprintf(stderr, "msgrcv failed with errno: %d\n", errno);
                exit(EXIT_FAILURE);
            }
            printf("You wrote: %s\n",data.text);
            //遇到end结束
            if(strncmp(data.text, "end", 3) == 0)
                running = 0;
        }
        //删除消息队列
        if(msgctl(msgid, IPC_RMID, 0) == -1)
        {
            fprintf(stderr, "msgctl(IPC_RMID) failed\n");
            exit(EXIT_FAILURE);
        }
        exit(EXIT_SUCCESS);
    }
    

    ? 发送信息的程序的源文件msgsend.c的源代码为:

    #include <unistd.h>
    #include <stdlib.h>
    #include <stdio.h>
    #include <string.h>
    #include <sys/msg.h>
    #include <errno.h>
    
    #define MAX_TEXT 512
    struct msg_st
    {
        long int msg_type;
        char text[MAX_TEXT];
    };
    
    int main()
    {
        int running = 1;
        struct msg_st data;
        char buffer[BUFSIZ];
        int msgid = -1;
    
        //建立消息队列
        msgid = msgget((key_t)1234, 0666 | IPC_CREAT);
        if(msgid == -1)
        {
            fprintf(stderr, "msgget failed with error: %d\n", errno);
            exit(EXIT_FAILURE);
        }
    
        //向消息队列中写消息,直到写入end
        while(running)
        {
            //输入数据
            printf("Enter some text: ");
            fgets(buffer, BUFSIZ, stdin);
            data.msg_type = 1;    //注意2
            strcpy(data.text, buffer);
            //向队列发送数据
            if(msgsnd(msgid, (void*)&data, MAX_TEXT, 0) == -1)
            {
                fprintf(stderr, "msgsnd failed\n");
                exit(EXIT_FAILURE);
            }
            //输入end结束输入
            if(strncmp(buffer, "end", 3) == 0)
                running = 0;
            sleep(1);
        }
        exit(EXIT_SUCCESS);
    }
    

    ? 运行结果如下:

    biao@ubuntu:~/test/msgRecvSend$
    biao@ubuntu:~/test/msgRecvSend$ ls
    msgreceive.c  msgsend.c  recv  send
    biao@ubuntu:~/test/msgRecvSend$ ./recv &
    [1] 8753
    biao@ubuntu:~/test/msgRecvSend$ ./send
    Enter some text: helloworld
    You wrote: helloworld
    
    Enter some text: Caibiao Lee
    You wrote: Caibiao Lee
    
    Enter some text: end
    You wrote: end
    
    [1]+  Done                    ./recv
    biao@ubuntu:~/test/msgRecvSend$
    
  2. ? 共享存储SharedMemory:共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号两,配合使用,来实现进程间的同步和通信。

    /* Linux 6.cpp */
    #include <iostream>
    #include <stdlib.h>
    #include <string.h>
    #include <sys/shm.h>
    #include <sys/ipc.h>
    #include <unistd.h>
    
    using namespace std;
    int main()
    {
      char *shmaddr;
      char *shmaddread;
      char str[]="Hello, I am a processing. \n";
      int shmid;
    
      key_t key = ftok(".",1);
      pid_t pid1 = fork();
      if(pid1 == -1){
        cout << "Fork error. " << endl;
        exit(1);
      }
      else if(pid1 == 0){
        //子进程
        shmid = shmget(key,1024,IPC_CREAT | 0600);
        shmaddr = (char*)shmat(shmid, NULL, 0);
                   strcpy(shmaddr, str);
        cout << "[Writer] write: " << shmaddr << endl;
        shmdt(shmaddr);
      }
      else
      {
        //父进程
        pid_t pid2 = fork();
        if(pid2 == -1){
          cout << "Fork error. " << endl;
          exit(1);
        }
        else if(pid2 == 0){
          //子进程
          sleep(2);
          shmid = shmget(key,1024,IPC_CREAT | 0600);
          shmaddread = (char*)shmat(shmid, NULL, 0);        
          cout << "[Reader] read: " << shmaddread << endl;
          shmdt(shmaddread);
        }
      }
      sleep(3);
      return 0;
    }
    
  3. ? 信号量Semaphore:信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。 使用示例代码如下

    //g++ semtest.cpp -o test -lpthread
     #include <stdio.h>
     #include <semaphore.h>
     #include <pthread.h>
     #include <unistd.h>
     #include <sys/time.h>
     sem_t sem;
    
     /*function:获取当前时间,精确到毫秒
    
  • */
    int64_t getTimeMsec()
    {
    struct timeval tv;
    gettimeofday(&tv, NULL);
    return tv.tv_sec * 1000 + tv.tv_usec / 1000;
    }

    void* func_sem_wait(void* arg)
    {
    printf(“set wait\n”);
    sem_wait(&sem);
    printf(“sem wait success\n”);
    int running = (int)arg;
    printf(“func_sem_wait running\n”);
    printf(“%d\n”, *running);
    }

    void* func_sem_timedwait(void* arg)
    {
    timespec timewait;
    timewait.tv_sec = getTimeMsec() / 1000 + 2;
    timewait.tv_nsec = 0;
    printf(“sem_timedwait\n”);
    int ret = sem_timedwait(&sem, &timewait);
    printf(“sem_timedwait,ret=%d\n”, ret);
    printf(“func_sem_timedwait running\n”);
    }

    void* func_sem_post(void* arg)
    {
    printf(“func_sem_post running\n”);
    printf(“sem post\n”);
    int a = (int)arg;
    *a = 6;
    sem_post(&sem);
    sem_post(&sem);
    }

    int main()
    {
    sem_init(&sem, 0, 0);
    pthread_t thread[3];
    int a = 5;

    pthread_create(&(thread[0]), NULL, func_sem_wait, &a);
     printf("thread func_sem_wait\n");
    
    pthread_create(&(thread[2]), NULL, func_sem_timedwait, &a);
     printf("thread func_sem_timedwait\n");
    
    sleep(4);
    
    pthread_create(&(thread[1]), NULL, func_sem_post, &a);
     printf("thread func_sem_post\n");
    
    pthread_join(thread[0], NULL);
     pthread_join(thread[1], NULL);
     pthread_join(thread[2], NULL);
     sem_destroy(&sem);
    

    }

    
    
  1. ? 套接字Socket:套解口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同机器间的进程通信。

  2. ? 信号 ( sinal ) : 信号是进程间通信机制中唯一的异步通信机制,可以看作是异步通知,通知接收信号的进程有哪些事情发生了。也可以简单理解为信号是某种形式上的软中断。

    ? 一般情况下,信号的来源可分为以下三种:

  • ? 硬件方式:除数为零、无效的存储访问等硬件异常产生信号。这些事件通常由硬件(如:CPU)检测到,并将其通知给Linux操作系统内核,然后内核生成相应的信号,并把信号发送给该事件发生时正在进行的程序。

  • ? 软件方式:用户在终端下调用kill命令向进程发送任务信号、进程调用kill或sigqueue函数发送信号、当检测到某种软件条件已经具备时发出信号,如由alarm或settimer设置的定时器超时时将生成SIGALRM信号等多种情景均可产生信号。

  • ? 键盘输入:当用户在终端上按下某键时,将产生信号。如按下组合键Ctrl+C将产生一个SIGINT信号,Ctrl+\产生一个SIGQUIT信号等。

    ? 以下列出几个常用的信号:

    信号描述
    SIGHUP当用户退出终端时,由该终端开启的所有进程都退接收到这个信号,默认动作为终止进程。
    SIGINT程序终止(interrupt)信号, 在用户键入INTR字符(通常是Ctrl+C)时发出,用于通知前台进程组终止进程。
    SIGQUIT和SIGINT类似, 但由QUIT字符(通常是Ctrl+)来控制. 进程在因收到SIGQUIT退出时会产生core文件, 在这个意义上类似于一个程序错误信号。
    SIGKILL用来立即结束程序的运行. 本信号不能被阻塞、处理和忽略
    SIGTERM程序结束(terminate)信号, 与SIGKILL不同的是该信号可以被阻塞和处理。通常用来要求程序自己正常退出。
    SIGSTOP停止(stopped)进程的执行. 注意它和terminate以及interrupt的区别:该进程还未结束, 只是暂停执行. 本信号不能被阻塞, 处理或忽略.

? 代码示例:

? 下面的代码收到程序退出信号后会执行用户定义的信号处理函数来替代系统默认的处理程序。

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

void sig_handle(int sig) {
    printf("received signal: %d, quit.\n", sig);
    exit(0);
}

int main () {
    signal(SIGINT, sig_handle);
    signal(SIGKILL, sig_handle);
    signal(SIGSEGV, sig_handle);
    signal(SIGTERM, sig_handle);

    int i = 0;
    while (1) {
        printf("%d\n", ++i);
        sleep(2);
    }

    printf("main quit.");

    return 0;
}
复制代码

? 运行结果:

1
2
received signal: 15, quit.

6.请介绍线程之间的通信方式。

  1. ? 锁机制:包括互斥锁、条件变量、读写锁互斥锁提供了以排他方式防止数据结构被并发修改的方法。读写锁允许多个线程同时读共享数据,而对写操作是互斥的。条件变量可以以原子的方式阻塞进程,直到某个特定条件为真为止。对条件的测试是在互斥锁的保护下进行的。条件变量始终与互斥锁一起使用。
  2. ? 信号量机制(Semaphore):包括无名线程信号量和命名线程信号量
  3. ? 信号机制(Signal):类似进程间的信号处理线程间的通信目的主要是用于线程同步,所以线程没有像进程通信中的用于数据交换的通信机制。

7.说一说进程的状态。

  1. ? 进程的3种基本状态:运行、就绪和阻塞。

? (1)就绪:当一个进程获得了除处理机以外的一切所需资源,一旦得到处理机即可运行,则称此进程处于就绪状态。就绪进程可以按多个优先级来划分队列。例如,当一个进程由于时间片用完而进入就绪状态时,排入低优先级队列;当进程由I/O操作完成而进入就绪状态时,排入高优先级队列。

? (2)运行:当一个进程在处理机上运行时,则称该进程处于运行状态。处于此状态的进程的数目小于等于处理器的数目,对于单处理机系统,处于运行状态的进程只有一个。在没有其他进程可以执行时(如所有进程都在阻塞状态),通常会自动执行系统的空闲进程。

? (3)阻塞:也称为等待或睡眠状态,一个进程正在等待某一事件发生(例如请求I/O而等待I/O完成等)而暂时停止运行,这时即使把处理机分配给进程也无法运行,故称该进程处于阻塞状态。

? 其转移图如下:

? img

  1. ? 进程的五种状态

    ? 创建状态:进程在创建时需要申请一个空白PCB,向其中填写控制和管理进程的信息,完成资源分配。如果创建工作无法完成,比如资源无法满足,就无法被调度运行,把此时进程所处状态称为创建状态

    ? 就绪状态:进程已经准备好,已分配到所需资源,只要分配到CPU就能够立即运行

    ? 执行状态:进程处于就绪状态被调度后,进程进入执行状态

    ? 阻塞状态:正在执行的进程由于某些事件(I/O请求,申请缓存区失败)而暂时无法运行,进程受到阻塞。在满足请求时进入就绪状态等待系统调用

    ? 终止状态:进程结束,或出现错误,或被系统终止,进入终止状态。无法再执行

? img

8. CPU调度的最小单位是什么?线程需要CPU调度吗?

  1. ? 进程是CPU分配资源的最小单位,线程是CPU调度的最小单位。
  2. ? 线程是比进程更小的能独立运行的基本单位,需要通过CPU调度来切换上下文,达到并发的目的。

9.进程之间共享内存的通信方式有什么好处?

? 采用共享内存通信的一个显而易见的好处是效率高,因为进程可以直接读写内存,而不需要任何数据的拷贝。对于像管道和消息队列等通信方式,则需要在内核和用户空间进行四次的数据拷贝,而共享内存则只拷贝两次数据:一次从输入文件到共享内存区,另一次从共享内存区到输出文件。

? 实际上,进程之间在共享内存时,并不总是读写少量数据后就解除映射,有新的通信时,再重新建立共享内存区域。而是保持共享区域,直到通信完毕为止,这样,数据内容一直保存在共享内存中,并没有写回文件。共享内存中的内容往往是在解除映射时才写回文件的。因此,采用共享内存的通信方式效率是非常高的。

10. 如何杀死一个进程?

  1. ? 杀死父进程并不会同时杀死子进程:每个进程都有一个父进程。可以使用 pstree 或 ps 工具来观察这一点。

    # 启动两个虚拟进程
    $ sleep 100 &
    $ sleep 101 &
    
    $ pstree -p
    init(1)-+
            |-bash(29051)-+-pstree(29251)
                          |-sleep(28919)
                          `-sleep(28964)
    
    $ ps j -A
     PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
        0     1     1     1 ?           -1 Ss       0   0:03 /sbin/init
    29051  1470  1470 29051 pts/2     2386 SN    1000   0:00 sleep 100
    29051  1538  1538 29051 pts/2     2386 SN    1000   0:00 sleep 101
    29051  2386  2386 29051 pts/2     2386 R+    1000   0:00 ps j -A
        1 29051 29051 29051 pts/2     2386 Ss    1000   0:00 -bash
    

    ? 调用 ps 命令可以显示 PID(进程 ID) 和 PPID(父进程 ID)。

    ? 杀死父进程后,子进程将会成为孤儿进程,而 init 进程将重新成为它的父进程。

  2. ? 杀死进程组或会话中的所有进程

    $ kill -SIGTERM -- -19701
    

    ? 这里用一个负数 -19701 向进程组发送信号。如果传递的是一个正数,这个数将被视为进程 ID 用于终止进程。如果传递的是一个负数,它被视为 PGID,用于终止整个进程组。负数来自系统调用的直接定义。

    ? 杀死会话中的所有进程与之完全不同。即使是具有会话 ID 的系统,例如 Linux,也没有提供系统调用来终止会话中的所有进程。需要遍历 /proc 输出的进程树,收集所有的 SID,然后一一终止进程。

    ? Pgrep 实现了遍历、收集并通过会话 ID 杀死进程的算法。可以使用以下命令:

    pkill -s <SID>
    

11.说一说kill的原理。

  • ? kill 命令的执行原理是这样的,kill 命令会向操作系统内核发送一个信号(多是终止信号)和目标进程的 PID,然后系统内核根据收到的信号类型,对指定进程进行相应的操作。kill 命令的基本格式如下:

    ? [root@localhost ~]# kill [信号] PID

  • ? kill 命令是按照 PID 来确定进程的,所以 kill 命令只能识别 PID,而不能识别进程名。

  • ? kill 命令只是“发送”一个信号,因此,只有当信号被程序成功“捕获”,系统才会执行 kill 命令指定的操作;反之,如果信号被“封锁”或者“忽略”,则 kill 命令将会失效。

12. 介绍下你知道的锁。

  1. ? 悲观锁

? 悲观锁并不是某一个锁,是一个锁类型,无论是否并发竞争资源,都会锁住资源,并等待资源释放下一个线程才能获取到锁。 这明显很悲观,所以就叫悲观锁。这明显可以归纳为一种策略,只要符合这种策略的锁的具体实现,都是悲观锁的范畴。

  1. ? 乐观锁

? 与悲观锁相对的,乐观锁也是一个锁类型。当线程开始竞争资源时,不是立马给资源上锁,而是进行一些前后值比对,以此来操作资源。例如常见的CAS操作,就是典型的乐观锁。示例如下

int cas(long *addr, long old, long new) {
    /* 原子执行 */
    if(*addr != old)
        return 0;
    *addr = new;
    return 1;
}
  1. ? 自旋锁

? 自旋锁是一种基础的同步原语,用于保障对共享数据的互斥访问。与互斥锁的相比,在获取锁失败的时候不会使得线程阻塞而是一直自旋尝试获取锁。当线程等待自旋锁的时候,CPU不能做其他事情,而是一直处于轮询忙等的状态。

? 自旋锁主要适用于被持有时间短,线程不希望在重新调度上花过多时间的情况。实际上许多其他类型的锁在底层使用了自旋锁实现,例如多数互斥锁在试图获取锁的时候会先自旋一小段时间,然后才会休眠。如果在持锁时间很长的场景下使用自旋锁,则会导致CPU在这个线程的时间片用尽之前一直消耗在无意义的忙等上,造成计算资源的浪费。

// 用户空间用 atomic_flag 实现自旋互斥
#include <thread>
#include <vector>
#include <iostream>
#include <atomic>

std::atomic_flag lock = ATOMIC_FLAG_INIT;

void f(int n)
{
    for (int cnt = 0; cnt < 100; ++cnt) {
        while (lock.test_and_set(std::memory_order_acquire))  // 获得锁
             ; // 自旋
        std::cout << "Output from thread " << n << '\n';
        lock.clear(std::memory_order_release);               // 释放锁
    }
}

int main()
{
    std::vector<std::thread> v;
    for (int n = 0; n < 10; ++n) {
        v.emplace_back(f, n);
    }
    for (auto& t : v) {
        t.join();
    }
}
  1. ? 公平锁

? 多个线程竞争同一把锁,如果依照先来先得的原则,那么就是一把公平锁。

  1. ? 非公平锁

? 多个线程竞争锁资源,抢占锁的所有权。

  1. ? 共享锁

? 多个线程可以共享这个锁的拥有权。一般用于数据的读操作,防止数据被写修改。共享锁的代码示例如下:

    #include <shared_mutex>
    #include <mutex>
    #include <iostream>
    #include <thread>
    #include <chrono>

    std::shared_mutex test_lock;

    std::mutex cout_lock;

    int arr[3] = {11, 22, 33};

    void unique_lock_demo(int id)
    {
        std::unique_lock lock{test_lock};

        for(int i =0; i < 3; i++)
        {
                arr[i] = i + 100 * id;
        }

        for(int i = 0; i < 3; i++)
        {
            std::unique_lock pl(cout_lock);
            std::cout << "In unique: " << id << ": " << arr[i] << std::endl;
            pl.unlock();
            std::this_thread::sleep_for(std::chrono::seconds(1));
        }
    }


    void shared_lock_demo(int id)
    {
        std::shared_lock lock{test_lock};

        for(int i = 0; i < 3; i++)
        {
            std::unique_lock pl(cout_lock);
            std::cout << "In shared " << id << ": " << arr[i] << std::endl;
            pl.unlock();
            std::this_thread::sleep_for(std::chrono::seconds(1));
        }
    }

    int main()
    {

       std::thread t3(unique_lock_demo,3);
       std::thread t4(unique_lock_demo,4);
       std::thread t1(shared_lock_demo,1);
       std::thread t2(shared_lock_demo,2);

       t1.join();
       t2.join();
       t3.join();
       t4.join();
       return 0;
    }

? 输出为:

    In unique: 3: 300
    In unique: 3: 301
    In unique: 3: 302
    In shared 1: 300
    In shared 2: 300
    In shared 1: 301
    In shared 2: 301
    In shared 1: 302
    In shared 2: 302
    In unique: 4: 400
    In unique: 4: 401
    In unique: 4: 402

? 从这个输出可以看出:

  • ? 如果一个线程已经获取了共享锁,则其他任何线程都无法获取互斥锁,但是可以获取共享锁
  • ? 从这个输出可以看出,验证了如果一个线程已经获取了互斥锁,则其他线程都无法获取该锁。
  1. ? 死锁

? 死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。

mutex;   //代表一个全局互斥对象
void  A()
{
    mutex.lock();
    //这里操作共享数据
    B();  //这里调用B方法
    mutex.unlock();
    return;
}
void  B()
{
    mutex.lock();
    //这里操作共享数据
    mutex.unlock();
    return;
}

13.什么情况下会产生死锁?

? 如果在计算机系统中同时具备下面四个必要条件时,那么会发生死锁。换句话说,只要下面四个条件有一个不具备,系统就不会出现死锁。

  1. ? 互斥条件。即某个资源在一段时间内只能由一个进程占有,不能同时被两个或两个以上的进程占有。这种独占资源如CD-ROM驱动器,打印机等等,必须在占有该资源的进程主动释放它之后,其它进程才能占有该资源。这是由资源本身的属性所决定的。如独木桥就是一种独占资源,两方的人不能同时过桥。

    #include <list>
    #include <mutex>
    #include <algorithm>
    
    std::list<int> some_list;    // 1
    std::mutex some_mutex;    // 2
    
    void add_to_list(int new_value)
    {
      std::lock_guard<std::mutex> guard(some_mutex);    // 3
      some_list.push_back(new_value);
    }
    
    bool list_contains(int value_to_find)
    {
      std::lock_guard<std::mutex> guard(some_mutex);    // 4
      return std::find(some_list.begin(),some_list.end(),value_to_find) != some_list.end();
    }
    

    ? 代码中有一个全局变量①,这个全局变量被一个全局的互斥量保护②。add_to_list()③和list_contains()④函数中使用std::lock_guardstd::mutex,使得这两个函数中对数据的访问是互斥的:list_contains()不可能看到正在被add_to_list()修改的列表。

  2. ? 不剥夺条件。进程所获得的资源在未使用完毕之前,资源申请者不能强行地从资源占有者手中夺取资源,而只能由该资源的占有者进程自行释放。如过独木桥的人不能强迫对方后退,也不能非法地将对方推下桥,必须是桥上的人自己过桥后空出桥面(即主动释放占有资源),对方的人才能过桥。

    
    
  3. ? 请求和保持条件。进程至少已经占有一个资源,但又申请新的资源;由于该资源已被另外进程占有,此时该进程阻塞;但是,它在等待新资源之时,仍继续占用已占有的资源。还以过独木桥为例,甲乙两人在桥上相遇。甲走过一段桥面(即占有了一些资源),还需要走其余的桥面(申请新的资源),但那部分桥面被乙占有(乙走过一段桥面)。甲过不去,前进不能,又不后退;乙也处于同样的状况。

    
    
  4. ? 循环等待条件。存在一个进程等待序列{P1,P2,…,Pn},其中P1等待P2所占有的某一资源,P2等待P3所占有的某一源,…,而Pn等待P1所占有的的某一资源,形成一个进程循环等待环。就像前面的过独木桥问题,甲等待乙占有的桥面,而乙又等待甲占有的桥面,从而彼此循环等待。

    std::mutex m;
    void f()
    {
    // ....
    std::lock_guard lock(m); // 1 子线程锁住互斥量m
    // ...
    }
    int main()
    {
    std::thread t(f);
    std::lock_guard lock(m); // 2 主线程锁住互斥量m
    // ...
    t.join(); // 3 等待子线程结束
    return 0;
    }
    

    ? 上述过程可能导致在2处上锁,然后子线程在1处发生阻塞,最后主线程在3处一直等待子线程结束,无穷等待下去。

? 上面提到的这四个条件在死锁时会同时发生。也就是说,只要有一个必要条件不满足,则死锁就可以排除。

14. 说一说你对自旋锁的理解。

旋锁的定义:当一个线程尝试去获取某一把锁的时候,如果这个锁此时已经被别人获取(占用),那么此线程就无法获取到这把锁,该线程将会等待,间隔一段时间后会再次尝试获取。这种采用循环加锁 -> 等待的机制被称为自旋锁(spinlock)。

? 自旋锁有以下特点

  • ? 用于临界区互斥
  • ? 在任何时刻最多只能有一个执行单元获得锁
  • ? 要求持有锁的处理器所占用的时间尽可能短
  • ? 等待锁的线程进入忙循环

? 自旋锁存在的问题

  • ? 如果某个线程持有锁的时间过长,就会导致其它等待获取锁的线程进入循环等待,消耗CPU。使用不当会造成CPU使用率极高。
  • ? 无法满足等待时间最长的线程优先获取锁。不公平的锁就会存在“线程饥饿”问题。

? 自旋锁的优点

  • ? 自旋锁不会使线程状态发生切换,一直处于用户态,即线程一直都是active的;不会使线程进入阻塞状态,减少了不必要的上下文切换,执行速度快
  • ? 非自旋锁在获取不到锁的时候会进入阻塞状态,从而进入内核态,当获取到锁的时候需要从内核态恢复,需要线程上下文切换。(线程被阻塞后便进入内核(Linux)调度状态,这个会导致系统在用户态与内核态之间来回切换,严重影响锁的性能)

? 自旋锁与互斥锁的区别

  • ? 自旋锁与互斥锁都是为了实现保护资源共享的机制。
  • ? 无论是自旋锁还是互斥锁,在任意时刻,都最多只能有一个保持者。
  • ? 获取互斥锁的线程,如果锁已经被占用,则该线程将进入睡眠状态;获取自旋锁的线程则不会睡眠,而是一直循环等待锁释放。
文章来源:https://blog.csdn.net/javayoungcoolboy/article/details/135317946
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。